Refactored v3 (#350)
This PR contains refactored code.
**Changelog**
- Added new net code (the communication architecture was left intact).
- All network client IDs now have custom type `network.Uid` backed by github.com/rs/xid lib.
```
The string representation of a UUID takes 32 bytes, and the new type will take just 16.
Because of Golang JSON serialization problems with omitting zero-length empty slices (it can't)
and the need to use UID values as map keys (maps don't support slices as keys),
IDs are stored as strings (for now).
```
- A whole new WebSocket client/server implementation was added, as well as a new communication layer with synchronous and async call handlers.
- WebSocket connections now support dedicated Ping/Pong frames as opposed to original ping text messages.
- Used Gorilla WebSocket library doesn't allow concurrent (simultaneous) reads and writes, so this part was handled via send channel synchronization.
- New API structures can be found in the `pkg/api` folder.
- New communication protocol can be found in the `pkg/com/*` folder.
- Updated communication protocol is based on JSON-encoded messaging through WebSocket and has the following structure:
```
Packet
[id] string — a globally unique identification tag for the packet to track it trough a chain of requests.
t uint8 — contains packet type information (i.e. INIT_PACKET, SDP_OFFER_PACKET, ...).
[p] any — contains packet data (any type).
Each packet is a text message in JSON-serialized form (WebSocket control frames obviously not).
```
```
The main principle of this protocol and the duplex data exchange is:
the one who initializes connection is called a client, and
the one who is being connected to is called a server.
With the current architecture, the coordinator is the server, the user browsers and workers are the clients.
____ ____
↓ ↑ ↑ ↓
browser ⟶ coordinator ⟵ worker
(c) (s) (c)
One of the most crucial performance vise parts of these interactions is that
all the server-initiated calls to clients should be asynchronous!
```
- In order to track synchronous calls (packets) with an asynchronous protocol, such as WebSocket, each packet may have an `id` that should be copied in all subsequent requests/responses.
- The old `sessionID` param was replaced by `id` that should be stored inside the `p` (payload) part of the packet.
- It is possible to skip the default ping check for all connected workers on every user connection and just pick the first available with the new roundRobin param in the coordinator config file `coordinator.roundRobin: true/false`.
- Added a dedicated package for the system API (pkg/api/*).
- Added structured logging system (zerolog) for better logging and cloud services integration.
- Added a visual representation of the network message exchange in logs:
```
...
01:00:01.1078 3f98 INF w → c Handshake ws://localhost:8000/wso
01:00:01.1138 994 INF c ← w Handshake localhost:8000
01:00:01.1148 994 INF c ← w Connect cid=cep.hrg
01:00:01.1158 994 DBG c connection id has been changed to cepl7obdrc3jv66kp2ug cid=cep.hrg
01:00:01.1158 3f98 INF w → c Connect cid=cep.2ug
01:00:01.1158 994 INF c New worker / addr: localhost, ...
01:00:01.1158 3f98 INF w Connected to the coordinator localhost:8000 cid=cep.2ug
01:00:02.5834 994 INF c ← u Handshake localhost:8000
01:00:02.6175 994 INF c ← u Connect cid=cep.hs0
01:00:02.6209 994 INF c Search available workers cid=cep.hs0
01:00:02.6214 994 INF c Found next free worker cid=cep.hs0
01:00:02.6220 994 INF c → u InitSession cid=cep.hs0
01:00:02.6527 994 INF c ← u WebrtcInit cid=cep.hs0
01:00:02.6527 994 INF c → w ᵇWebrtcInit cid=cep.hrg
01:00:02.6537 3f98 INF w ← c WebrtcInit cid=cep.2ug
01:00:02.6537 3f98 INF w WebRTC start cid=cep.2ug
...
```
- Replaced a monstrous Prometheus metrics lib.
- Removed spflag dependency.
- Added new `version` config file param/constant for compatibility reasons.
- Bump the minimum required version for Go to 1.18 due to use of generics.
- Opus encoder now is cached and the default config is 96Kbps, complexity 5 (was 196Kbps, 8).
- Changed the default x264 quality parameters to `crf 23 / superfast / baseline` instead of `crf 17 / veryfast / main`.
- Added a separate WebRTC logging config param `webrtc.logLevel`.
- Worker now allocates much less memory.
- Optimized and fixed RGB to YUV converter.
- `--v=5` logging cmd flag was removed and replaced with the `debug` config parameter.
**Breaking changes (migration to v3)**
- Coordinator server API changes, see web/js/api/api.js.
- Coordinator client event API changes:
- c `GAME_PLAYER_IDX_CHANGE` (string) -> `GAME_PLAYER_IDX` (number)
- c `GAME_PLAYER_IDX` -> `GAME_PLAYER_IDX_SET`
- c `MEDIA_STREAM_INITIALIZED` -> `WEBRTC_NEW_CONNECTION`
- c `MEDIA_STREAM_SDP_AVAILABLE` -> `WEBRTC_SDP_OFFER`
- c `MEDIA_STREAM_CANDIDATE_ADD` -> `WEBRTC_ICE_CANDIDATE_RECEIVED`
- c `MEDIA_STREAM_CANDIDATE_FLUSH` -> `WEBRTC_ICE_CANDIDATES_FLUSH`
- x `MEDIA_STREAM_READY` -> **removed**
- c `CONNECTION_READY` -> `WEBRTC_CONNECTION_READY`
- c `CONNECTION_CLOSED` -> `WEBRTC_CONNECTION_CLOSED`
- c `GET_SERVER_LIST` -> `WORKER_LIST_FETCHED`
- x `KEY_STATE_UPDATED` -> **removed**
- n `WEBRTC_ICE_CANDIDATE_FOUND`
- n `WEBRTC_SDP_ANSWER`
- n `MESSAGE`
- `rtcp` module renamed to `webrtc`.
- Controller state equals Libretro controller state (changed order of bits), see: web/js/input/input.js.
- Added new `coordintaor.selector` config param that changes the selection algorithm for workers. By default it will select any free worker. Set this param to `ping` for the old behavior.
- Changed the name of the `webrtc.iceServers.url` config param to `webrtc.iceServers.urls`.
4
.github/workflows/cd/docker-compose.yml
vendored
|
|
@ -18,7 +18,7 @@ services:
|
|||
|
||||
coordinator:
|
||||
<<: *default-params
|
||||
command: coordinator --v=5
|
||||
command: coordinator
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
|
||||
|
|
@ -28,7 +28,7 @@ services:
|
|||
environment:
|
||||
- MESA_GL_VERSION_OVERRIDE=3.3
|
||||
entrypoint: [ "/bin/sh", "-c", "xvfb-run -a $$@", "" ]
|
||||
command: worker --v=5 --zone=${ZONE:-}
|
||||
command: worker --zone=${ZONE:-}
|
||||
volumes:
|
||||
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
|
||||
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ^1.18
|
||||
go-version: ^1.19
|
||||
|
||||
- name: Get Linux dev libraries and tools
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
4
.gitignore
vendored
|
|
@ -67,6 +67,9 @@ _output/
|
|||
./build
|
||||
release/
|
||||
vendor/
|
||||
tests/
|
||||
!tests/e2e/
|
||||
*.exe
|
||||
|
||||
.dockerignore
|
||||
|
||||
|
|
@ -75,3 +78,4 @@ fbneo/
|
|||
hi/
|
||||
nvram/
|
||||
*.mcd
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y \
|
|||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# go setup layer
|
||||
ARG GO=go1.18.linux-amd64.tar.gz
|
||||
ARG GO=go1.19.linux-amd64.tar.gz
|
||||
RUN wget -q https://golang.org/dl/$GO \
|
||||
&& rm -rf /usr/local/go \
|
||||
&& tar -C /usr/local -xzf $GO \
|
||||
|
|
|
|||
72
Makefile
|
|
@ -1,11 +1,10 @@
|
|||
# Makefile includes some useful commands to build or format incentives
|
||||
# More commands could be added
|
||||
|
||||
# Variables
|
||||
PROJECT = cloud-game
|
||||
REPO_ROOT = github.com/giongto35
|
||||
ROOT = ${REPO_ROOT}/${PROJECT}
|
||||
|
||||
CGO_CFLAGS='-g -O3 -funroll-loops'
|
||||
CGO_LDFLAGS='-g -O3'
|
||||
|
||||
fmt:
|
||||
@goimports -w cmd pkg tests
|
||||
@gofmt -s -w cmd pkg tests
|
||||
|
|
@ -13,47 +12,6 @@ fmt:
|
|||
compile: fmt
|
||||
@go install ./cmd/...
|
||||
|
||||
check: fmt
|
||||
@golangci-lint run cmd/... pkg/...
|
||||
# @staticcheck -checks="all,-S1*" ./cmd/... ./pkg/... ./tests/...
|
||||
|
||||
dep:
|
||||
go mod download
|
||||
# go mod tidy
|
||||
|
||||
# NOTE: there is problem with go mod vendor when it delete github.com/gen2brain/x264-go/x264c causing unable to build. https://github.com/golang/go/issues/26366
|
||||
#build.cross: build
|
||||
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-darwin ./cmd/coordinator
|
||||
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-darwin ./cmd/worker
|
||||
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-linu ./cmd/coordinator
|
||||
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-linux ./cmd/worker
|
||||
|
||||
# A user can invoke tests in different ways:
|
||||
# - make test runs all tests;
|
||||
# - make test TEST_TIMEOUT=10 runs all tests with a timeout of 10 seconds;
|
||||
# - make test TEST_PKG=./model/... only runs tests for the model package;
|
||||
# - make test TEST_ARGS="-v -short" runs tests with the specified arguments;
|
||||
# - make test-race runs tests with race detector enabled.
|
||||
TEST_TIMEOUT = 60
|
||||
TEST_PKGS ?= ./cmd/... ./pkg/...
|
||||
TEST_TARGETS := test-short test-verbose test-race test-cover
|
||||
.PHONY: $(TEST_TARGETS) test tests
|
||||
test-short: TEST_ARGS=-short
|
||||
test-verbose: TEST_ARGS=-v
|
||||
test-race: TEST_ARGS=-race
|
||||
test-cover: TEST_ARGS=-cover
|
||||
$(TEST_TARGETS): test
|
||||
|
||||
test: compile
|
||||
@go test -timeout $(TEST_TIMEOUT)s $(TEST_ARGS) $(TEST_PKGS)
|
||||
|
||||
test-e2e: compile
|
||||
@go test ./tests/e2e/...
|
||||
|
||||
cover:
|
||||
@go test -v -covermode=count -coverprofile=coverage.out $(TEST_PKGS)
|
||||
# @$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $(COVERALLS_TOKEN)
|
||||
|
||||
clean:
|
||||
@rm -rf bin
|
||||
@rm -rf build
|
||||
|
|
@ -62,25 +20,33 @@ clean:
|
|||
build:
|
||||
mkdir -p bin/
|
||||
go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
|
||||
go build -buildmode=exe -tags static -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) -o bin/ ./cmd/worker
|
||||
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
|
||||
go build -buildmode=exe -tags static \
|
||||
-ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) \
|
||||
-o bin/ ./cmd/worker
|
||||
|
||||
verify-cores:
|
||||
go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "../../../_rendered"
|
||||
go test -run TestAllEmulatorRooms ./pkg/worker -v -renderFrames $(GL_CTX) -outputPath "../../_rendered"
|
||||
|
||||
dev.build: compile build
|
||||
|
||||
dev.build-local:
|
||||
mkdir -p bin/
|
||||
go build -o bin/ ./cmd/coordinator
|
||||
go build -o bin/ ./cmd/worker
|
||||
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} go build -o bin/ ./cmd/worker
|
||||
|
||||
dev.run: dev.build-local
|
||||
./bin/coordinator --v=5 &
|
||||
./bin/worker --v=5
|
||||
./bin/coordinator & ./bin/worker
|
||||
|
||||
dev.run.debug:
|
||||
go build -race -o bin/ ./cmd/coordinator
|
||||
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
|
||||
go build -race -gcflags=all=-d=checkptr -o bin/ ./cmd/worker
|
||||
./bin/coordinator & ./bin/worker
|
||||
|
||||
dev.run-docker:
|
||||
docker rm cloud-game-local -f || true
|
||||
CLOUD_GAME_GAMES_PATH=$(PWD)/assets/games docker-compose up --build
|
||||
docker-compose up --build
|
||||
|
||||
# RELEASE
|
||||
# Builds the app for new release.
|
||||
|
|
@ -97,8 +63,8 @@ dev.run-docker:
|
|||
# Config params:
|
||||
# - RELEASE_DIR: the name of the output folder (default: release).
|
||||
# - CONFIG_DIR: search dir for core config files.
|
||||
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; defalut: ldd).
|
||||
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., mylib.so; default: .*so).
|
||||
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; default: ldd).
|
||||
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., my_lib.so; default: .*so).
|
||||
# Be aware that this search pattern will return only matched regular expression part and not the whole line.
|
||||
# de. -> abc def ghj -> def
|
||||
# Makefile special symbols should be escaped with \.
|
||||
|
|
|
|||
|
|
@ -31,11 +31,6 @@ Because I only hosted the platform on limited servers in US East, US West, Eu, S
|
|||
latency issues + connection problem. You can try hosting the service following the instruction the next section to have
|
||||
a better sense of performance.
|
||||
|
||||
| Screenshot | Screenshot |
|
||||
| :--------------------------------------------: | :--------------------------------------------: |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Feature
|
||||
|
||||
1. **Cloud gaming**: Game logic and storage is hosted on cloud service. It reduces the cumbersome of game
|
||||
|
|
|
|||
1
assets/cores/nestopia_libretro.cfg
Normal file
|
|
@ -0,0 +1 @@
|
|||
nestopia_audio_type=stereo
|
||||
|
|
@ -1,40 +1,33 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
goflag "flag"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
config "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/os"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
|
||||
"github.com/golang/glog"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
var Version = ""
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
var Version = "?"
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
conf := config.NewConfig()
|
||||
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
|
||||
conf.ParseFlags()
|
||||
|
||||
logging.Init()
|
||||
defer logging.Flush()
|
||||
log := logger.NewConsole(conf.Coordinator.Debug, "c", true)
|
||||
|
||||
glog.Infof("[coordinator] version: %v", Version)
|
||||
glog.V(4).Infof("[coordinator] Local configuration %+v", conf)
|
||||
c := coordinator.New(conf)
|
||||
log.Info().Msgf("version %s", Version)
|
||||
log.Info().Msgf("conf version: %v", conf.Version)
|
||||
if log.GetLevel() < logger.InfoLevel {
|
||||
log.Debug().Msgf("config: %+v", conf)
|
||||
}
|
||||
c := coordinator.New(conf, log)
|
||||
c.Start()
|
||||
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
defer c.Shutdown(ctx)
|
||||
<-os.ExpectTermination()
|
||||
cancelCtx()
|
||||
if err := c.Stop(); err != nil {
|
||||
log.Error().Err(err).Msg("service shutdown errors")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,38 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
goflag "flag"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
config "github.com/giongto35/cloud-game/v2/pkg/config/worker"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/os"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/thread"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/worker"
|
||||
"github.com/golang/glog"
|
||||
flag "github.com/spf13/pflag"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/worker/thread"
|
||||
)
|
||||
|
||||
var Version = ""
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
}
|
||||
var Version = "?"
|
||||
|
||||
func run() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
conf := config.NewConfig()
|
||||
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
|
||||
conf.ParseFlags()
|
||||
|
||||
logging.Init()
|
||||
defer logging.Flush()
|
||||
log := logger.NewConsole(conf.Worker.Debug, "w", true)
|
||||
log.Info().Msgf("version %s", Version)
|
||||
log.Info().Msgf("conf version: %v", conf.Version)
|
||||
if log.GetLevel() < logger.InfoLevel {
|
||||
log.Debug().Msgf("config: %+v", conf)
|
||||
}
|
||||
|
||||
glog.Infof("[worker] version: %v", Version)
|
||||
glog.V(4).Infof("[worker] Local configuration %+v", conf)
|
||||
wrk := worker.New(conf)
|
||||
done := os.ExpectTermination()
|
||||
wrk := worker.New(conf, log, done)
|
||||
wrk.Start()
|
||||
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
defer wrk.Shutdown(ctx)
|
||||
<-os.ExpectTermination()
|
||||
cancelCtx()
|
||||
<-done
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := wrk.Stop(); err != nil {
|
||||
log.Error().Err(err).Msg("service shutdown errors")
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -2,14 +2,21 @@
|
|||
# Application configuration file
|
||||
#
|
||||
|
||||
# for the compatibility purposes
|
||||
version: 3
|
||||
|
||||
coordinator:
|
||||
# debugging switch
|
||||
# - shows debug logs
|
||||
# - allows selecting worker instances
|
||||
debug: false
|
||||
# selects free workers:
|
||||
# - any (default, any free)
|
||||
# - ping (with the lowest ping)
|
||||
selector: any
|
||||
# games library
|
||||
library:
|
||||
# some directory which is gonna be the root folder for the library
|
||||
# where games are stored
|
||||
# root folder for the library (where games are stored)
|
||||
basePath: assets/games
|
||||
# an explicit list of supported file extensions
|
||||
# which overrides Libretro emulator ROMs configs
|
||||
|
|
@ -54,6 +61,8 @@ coordinator:
|
|||
gtag:
|
||||
|
||||
worker:
|
||||
# show more logs
|
||||
debug: false
|
||||
network:
|
||||
# a coordinator address to connect to
|
||||
coordinatorAddress: localhost:8000
|
||||
|
|
@ -111,9 +120,14 @@ emulator:
|
|||
# special tag {user} will be replaced with current user's home dir
|
||||
storage: "{user}/.cr/save"
|
||||
|
||||
# path for storing emulator generated files
|
||||
localPath: "./libretro"
|
||||
|
||||
libretro:
|
||||
# use zip compression for emulator save states
|
||||
saveCompression: false
|
||||
saveCompression: true
|
||||
# Libretro cores logging level: DEBUG = 0, INFO, WARN, ERROR, DUMMY = INT_MAX
|
||||
logLevel: 1
|
||||
cores:
|
||||
paths:
|
||||
libs: assets/cores
|
||||
|
|
@ -176,6 +190,7 @@ emulator:
|
|||
roms: [ "zip" ]
|
||||
nes:
|
||||
lib: nestopia_libretro
|
||||
config: nestopia_libretro.cfg
|
||||
roms: [ "nes" ]
|
||||
snes:
|
||||
lib: snes9x_libretro
|
||||
|
|
@ -191,21 +206,23 @@ emulator:
|
|||
|
||||
encoder:
|
||||
audio:
|
||||
channels: 2
|
||||
# audio frame duration needed for WebRTC (Opus)
|
||||
frame: 20
|
||||
frequency: 48000
|
||||
# most of the emulators have ~1400 samples per a video frame,
|
||||
# so we keep the frame buffer roughly half of that size or 2 RTC packets per frame
|
||||
frame: 10
|
||||
video:
|
||||
# h264, vpx (VP8)
|
||||
codec: h264
|
||||
# concurrent execution units (0 - disabled)
|
||||
concurrency: 0
|
||||
# see: https://trac.ffmpeg.org/wiki/Encode/H.264
|
||||
h264:
|
||||
# Constant Rate Factor (CRF) 0-51 (default: 23)
|
||||
crf: 17
|
||||
crf: 23
|
||||
# ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
|
||||
preset: veryfast
|
||||
preset: superfast
|
||||
# baseline, main, high, high10, high422, high444
|
||||
profile: main
|
||||
profile: baseline
|
||||
# film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
|
||||
tune: zerolatency
|
||||
# 0-3
|
||||
|
|
@ -216,9 +233,6 @@ encoder:
|
|||
bitrate: 1200
|
||||
# force keyframe interval
|
||||
keyframeInterval: 5
|
||||
# run without a game
|
||||
# (experimental)
|
||||
withoutGame: false
|
||||
|
||||
# game recording
|
||||
# (experimental)
|
||||
|
|
@ -269,7 +283,7 @@ webrtc:
|
|||
dtlsRole:
|
||||
# a list of STUN/TURN servers to use
|
||||
iceServers:
|
||||
- url: stun:stun.l.google.com:19302
|
||||
- urls: stun:stun.l.google.com:19302
|
||||
# configures whether the ice agent should be a lite agent (true/false)
|
||||
# (performance)
|
||||
# don't use iceServers when enabled
|
||||
|
|
@ -287,3 +301,6 @@ webrtc:
|
|||
# override ICE candidate IP, see: https://github.com/pion/webrtc/issues/835,
|
||||
# can be used for Docker bridged network internal IP override
|
||||
iceIpMap:
|
||||
# set additional log level for WebRTC separately
|
||||
# -1 - trace, 6 - nothing, ..., debug - 0
|
||||
logLevel: 6
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ services:
|
|||
- 9000:9000
|
||||
- 8443:8443/udp
|
||||
command: >
|
||||
bash -c "Xvfb :99 & coordinator --v=5 & worker --v=5"
|
||||
bash -c "Xvfb :99 & coordinator & worker"
|
||||
volumes:
|
||||
# keep cores persistent in the cloud-game_cores volume
|
||||
- cores:/usr/local/share/cloud-game/assets/cores
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Web-based Cloud Gaming Service contains multiple workers for gaming stream and a
|
|||
## Worker
|
||||
|
||||
Worker is responsible for streaming game to frontend
|
||||

|
||||

|
||||
|
||||
- After Coordinator matches the most appropriate server to the user, webRTC peer-to-peer handshake will be conducted. The coordinator will exchange the signature (WebRTC Session Remote Description) between two peers over Web Socket connection.
|
||||
- On worker, each user session will spawn a new room running a gaming emulator. Image stream and audio stream from emulator is captured and encoded to WebRTC streaming format. We applied Vp8 for Video compression and Opus for audio compression to ensure the smoothest experience. After finish encoded, these stream is then piped out to user and observers joining that room.
|
||||
|
|
@ -16,7 +16,7 @@ Worker is responsible for streaming game to frontend
|
|||
|
||||
Coordinator is loadbalancer and coordinator, which is in charge of picking the most suitable workers for a user. Every time a user connects to Coordinator, it will collect all the metric from all workers, i.e free CPU resources and latency from worker to user. Coordinator will decide the best candidate based on the metric and setup peer-to-peer connection between worker and user based on WebRTC protocol
|
||||
|
||||

|
||||

|
||||
|
||||
1. A user connected to Coordinator .
|
||||
2. Coordinator will find the most suitable worker to serve the user.
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
## Streaming process description
|
||||
|
||||
This document describes the step-by-step process of media streaming in all parts of the application.
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
|
||||
│ USER AGENT │ │ COORDINATOR │ │ WORKER...n │
|
||||
├──────────────┤ ├───────────────┤ ├──────────────┤
|
||||
│ TCP/WS ├──1──►│ WS ──────► WS │◄───┤ TCP/WS │
|
||||
│ │ │ ▲ 2 │ │ │ │
|
||||
│ │ │ └───────────┘ │ │ │
|
||||
│ │ └───────────────┘ │ │
|
||||
│ UDP/RTP │◄─────────────3────────────┤ UDP/RTP │
|
||||
│ AUDIO < │ OPUS │ AUDIO │
|
||||
│ VIDEO < │ VP8/H264 │ VIDEO │
|
||||
│ DATA > │ 010101 │ DATA │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
The app is based on WebRTC technology which allows the server to stream media and exchange data with ultra-low latencies. An essential part of these types of P2P connections is the signaling process. It's implemented as a custom text-based messaging protocol on top of WebSocket (quite similarly to [WAMP](https://wamp-proto.org)). The app supports both STUN and TURN protocols for NAT traversal or ICE. In terms of supported codecs, it can stream h264, VP8, and OPUS media.
|
||||
|
||||
The streaming process begins when a user opens the main application page (index.html) served by the coordinator.
|
||||
- The user's browser tries to open a new WebSocket connection to the coordinator — socket.init(roomId, zone) [web/js/network/socket.js:32](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L32)
|
||||
> In the initial WebSocket Upgrade request query it may send two params: roomId — an identification number for existing game rooms stored in the URL query of the application page (i.e. app.com/?id=xxxxxx), zone — or, more precisely, region — serves the purpose of CDN and geographical segmentation of the streaming.
|
||||
- On the coordinator side this request goes into a dedicated handler (/ws) — func (o *Server) WS(w http.ResponseWriter, r *http.Request) [pkg/coordinator/handlers.go:150](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/pkg/coordinator/handlers.go#L150)
|
||||
- There, it unconditionally accepts the WebSocket connection and tags it with some ID, so it will be listening to messages from the user's side. Here a new client connection should be considered as established.
|
||||
- Next, given provided query params, the coordinator tries to find a suitable worker whose job — directly stream games to a user.
|
||||
> This process of choosing the right worker is following: if there is no roomId param, then the coordinator gathers the full list of available workers, filters them by a zone value (if provided), returns the user a list of public URLs, which he can ping and send results back to the coordinator. After that, the coordinator links the fastest one with the user. Alternatively, if the user did provide some roomId, then the coordinator directly assigns a worker with that room (workers have 1:1 mapping to rooms or games).
|
||||
> All the information exchange initiated from the worker side is handled in a separate endpoint (/wso) [pkg/coordinator/handlers.go#L81](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/handlers.go#L81).
|
||||
- Coordinator sends to the user ICE servers and the list of games available for playing. That's handled in [web/js/network/socket.js:57](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L57).
|
||||
- From this point, the user's browser begins to initialize WebRTC connection to the worker — web/js/controller.js:413 → [web/js/network/rtcp.js:16](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L16).
|
||||
- First, it sends init request through the WebSocket connection to the coordinator handler in [pkg/coordinator/useragenthandlers.go:17](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go#L17).
|
||||
> Following a standard WebRTC call [negotiation procedure](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling), the coordinator acts as a mediator between users and workers. The signaling protocol here is a text messaging through WebSocket transport.
|
||||
- Coordinator notifies the user's worker that it wants to establish a new PeerConnection (call). That part is being handled in [pkg/worker/internalhandlers.go:42](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/worker/internalhandlers.go#L42). It is worth noting that it is a worker who makes SDP offer and waits for an SDP answer.
|
||||
- Worker initializes new WebRTC connection handler in func (w *WebRTC) StartClient(isMobile bool, iceCB OnIceCallback) (string, error) [pkg/webrtc/webrtc.go:103](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/webrtc/webrtc.go#L103).
|
||||
- Then through the coordinator it makes simultaneously an SDP offer as well as sends ICE candidates that are handled on the coordinator side (from the user) in [pkg/coordinator/useragenthandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go),
|
||||
(from the worker) in [pkg/coordinator/internalhandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/internalhandlers.go), and on the user side both in [web/js/network/socket.js:56](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/socket.js#L56) and inside [web/js/network/rtcp.js](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js).
|
||||
- Browser on the user's side after SDP offer links remote streams to the HTML Video element in [web/js/controller.js:417](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L417), makes SDP answer and gathers remote ICE candidates until it's done (if receive an empty ICE candidate).
|
||||
- For the user's side a successful WebRTC connection should be considered established when WebRTC datachannel is opened here [web/js/network/rtcp.js:31](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L31).
|
||||
*And that should be it for the streaming part.*
|
||||
> At this point all the connections should be successfully established and the user's ready for a game to start. The coordinator should notify the worker about that fact and the worker starts pushing media frames, listen to the input through the direct to the user WebRTC data channel.
|
||||
- Then the user may send the game start request to the coordinator in [web/js/controller.js:153](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L153).
|
||||
|
||||
#### Streaming requirements
|
||||
- Workers should not have any closed UDP ports to be able to provide suitable ICE candidates.
|
||||
- Coordinator should have at least one non-blocked TCP port (default: 8000) for HTTP/WebSocket signaling connections from users and workers.
|
||||
- Browser should not block WebRTC and support it (check [here](https://test.webrtc.org/)).
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# Web-based Cloud Gaming Service Implementation Document
|
||||
|
||||
## Code structure
|
||||
```
|
||||
.
|
||||
├── cmd: service entrypoint
|
||||
│ ├── main.go: Spawn coordinator or worker based on flag
|
||||
│ └── main_test.go
|
||||
├── static: static file for front end
|
||||
│ ├── js
|
||||
│ │ └── ws.js: client logic
|
||||
│ ├── game.html: frontend with gameboy ui
|
||||
│ └── index_ws.html: raw frontend without ui
|
||||
├── coordinator: coordinator
|
||||
│ ├── handlers.go: coordinator entrypoint
|
||||
│ ├── browser.go: router listening to browser
|
||||
│ └── worker.go: router listening to worker
|
||||
├── games: roms list, no code logic
|
||||
├── worker: integration between emulator + webrtc (communication)
|
||||
│ ├── room:
|
||||
│ │ ├── room.go: room logic
|
||||
│ │ └── media.go: video + audio encoding
|
||||
│ ├── handlers.go: worker entrypoint
|
||||
│ └── coordinator.go: router listening to coordinator
|
||||
├── emulator: emulator internal
|
||||
│ ├── nes: NES device internal
|
||||
│ ├── director.go: coordinator of views
|
||||
│ └── gameview.go: in game logic
|
||||
├── cws
|
||||
│ └── cws.go: socket multiplexer library, used for signaling
|
||||
└── webrtc: webrtc streaming logic
|
||||
```
|
||||
|
||||
## Room
|
||||
Room is a fundamental part of the system. Each user session will spawn a room with a game running inside. There is a pipeline to encode images and audio and stream them out from emulator to user. The pipeline also listens to all input and streams to the emulator.
|
||||
|
||||
## Worker
|
||||
Worker is an instance that can be provisioned to scale up the traffic. There are multiple rooms inside a worker. Worker will listen to coordinator events in `coordinator.go`.
|
||||
|
||||
## Coordinator
|
||||
Coordinator is the coordinator, which handles all communication with workers and frontend.
|
||||
Coordinator will pair up a worker and a user for peer streaming. In WebRTC handshaking, two peers need to exchange their signature (Session Description Protocol) to initiate a peerconnection.
|
||||
Events come from frontend will be handled in `coordinator/browser.go`. Events come from worker will be handled in `coordinator/worker.go`. Coordinator stays in the middle and relays handshake packages between workers and user.
|
||||
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 366 KiB |
|
Before Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 365 KiB |
|
Before Width: | Height: | Size: 308 KiB |
|
Before Width: | Height: | Size: 8.4 MiB |
|
Before Width: | Height: | Size: 26 KiB |
|
|
@ -1,28 +0,0 @@
|
|||
# Web-based Cloud Gaming Service Game Instruction
|
||||
|
||||
The game can be played on Desktop, Mobile (Android only). You can plug joystick to play with the game.
|
||||
|
||||
Click question mark on the top left to see game instruction.
|
||||
|
||||
## Key map on Desktop
|
||||
Game keymap follows
|
||||
Arrow keys to move
|
||||
H -> Show help
|
||||
C -> Start
|
||||
V -> Select
|
||||
Z -> A
|
||||
X -> B
|
||||
S -> Save (Save state)
|
||||
A -> Load (Load previous saved state)
|
||||
W -> Share your running game to other or you can keep it to continue playing the next time. Multiple people can access the same game for multiplayer or observation.
|
||||
F -> Full screen
|
||||
Q -> Quit the current game and go to menu screen.**NOTE**: we are facing some issue with quit, so it's better to refresh the page.
|
||||
|
||||
## Mobile play
|
||||
You can play the game on Android device. Make sure your Android has the version that support WebRTC. IOS doesn't support WebRTC streaming now.
|
||||
|
||||
The keys map are equivalent to Desktop. Press the button to fire input.
|
||||
|
||||
## Joystick
|
||||
The game also accepts joystick, so you can try plug in one and experience. It will be very fun!
|
||||
|
||||
63
go.mod
|
|
@ -1,32 +1,49 @@
|
|||
module github.com/giongto35/cloud-game/v2
|
||||
|
||||
go 1.13
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec
|
||||
github.com/fsnotify/fsnotify v1.5.1
|
||||
github.com/VictoriaMetrics/metrics v1.23.0
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/goccy/go-json v0.10.0
|
||||
github.com/gofrs/flock v0.8.1
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/golang/glog v1.0.0
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/kkyr/fig v0.3.1-0.20220103220255-711af35e3ee2
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pion/ice/v2 v2.2.3 // indirect
|
||||
github.com/pion/interceptor v0.1.10
|
||||
github.com/pion/rtp v1.7.11 // indirect
|
||||
github.com/pion/webrtc/v3 v3.1.27
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/prometheus/common v0.33.0 // indirect
|
||||
github.com/pion/dtls/v2 v2.1.5
|
||||
github.com/pion/interceptor v0.1.12
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/webrtc/v3 v3.1.50
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/veandco/go-sdl2 v0.4.20
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921
|
||||
golang.org/x/image v0.0.0-20220321031419-a8550c1d254a
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 // indirect
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
github.com/rs/zerolog v1.28.0
|
||||
github.com/veandco/go-sdl2 v0.4.28
|
||||
golang.org/x/crypto v0.5.0
|
||||
golang.org/x/image v0.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/ice/v2 v2.2.13 // indirect
|
||||
github.com/pion/mdns v0.0.5 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.10 // indirect
|
||||
github.com/pion/rtp v1.7.13 // indirect
|
||||
github.com/pion/sctp v1.8.5 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.11 // indirect
|
||||
github.com/pion/stun v0.3.5 // indirect
|
||||
github.com/pion/transport v0.14.1 // indirect
|
||||
github.com/pion/turn/v2 v2.0.9 // indirect
|
||||
github.com/pion/udp v0.1.1 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
golang.org/x/net v0.5.0 // indirect
|
||||
golang.org/x/sys v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
|
|
|||
588
go.sum
|
|
@ -1,188 +1,57 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
|
||||
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
|
||||
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec h1:4XvMn0XuV7qxCH22gbnR79r+xTUaLOSA0GW/egpO3SQ=
|
||||
github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec/go.mod h1:NbXoa59CCAGqtRm7kRrcZIk2dTCJMRVF8QI3BOD7isY=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/VictoriaMetrics/metrics v1.23.0 h1:WzfqyzCaxUZip+OBbg1+lV33WChDSu4ssYII3nxtpeA=
|
||||
github.com/VictoriaMetrics/metrics v1.23.0/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
|
||||
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
|
||||
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
|
||||
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
|
||||
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=
|
||||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/kkyr/fig v0.3.1-0.20220103220255-711af35e3ee2 h1:ZSDGtCWL8CSbDE/ZHdTivgDl8CwuHb8TpMeSKRGAhfk=
|
||||
github.com/kkyr/fig v0.3.1-0.20220103220255-711af35e3ee2/go.mod h1:fEnrLjwg/iwSr8ksJF4DxrDmCUir5CaVMLORGYMcz30=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
|
||||
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
|
|
@ -193,415 +62,186 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
|
|||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
|
||||
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
|
||||
github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio=
|
||||
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
|
||||
github.com/pion/ice/v2 v2.2.2/go.mod h1:vLI7dFqxw8zMSb9J+ca74XU7JjLhddgfQB9+BbTydCo=
|
||||
github.com/pion/ice/v2 v2.2.3 h1:kBVhmtMcI1L3bWDepilO9kKpCGpLQeppCuVxVS8obhE=
|
||||
github.com/pion/ice/v2 v2.2.3/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
|
||||
github.com/pion/interceptor v0.1.10 h1:DJ2GjMGm4XGIQgMJxuEpdaExdY/6RdngT7Uh4oVmquU=
|
||||
github.com/pion/interceptor v0.1.10/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
|
||||
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
|
||||
github.com/pion/ice/v2 v2.2.12 h1:n3M3lUMKQM5IoofhJo73D3qVla+mJN2nVvbSPq32Nig=
|
||||
github.com/pion/ice/v2 v2.2.12/go.mod h1:z2KXVFyRkmjetRlaVRgjO9U3ShKwzhlUylvxKfHfd5A=
|
||||
github.com/pion/ice/v2 v2.2.13 h1:NvLtzwcyob6wXgFqLmVQbGB3s9zzWmOegNMKYig5l9M=
|
||||
github.com/pion/ice/v2 v2.2.13/go.mod h1:eFO4/1zCI+a3OFVt7l7kP+5jWCuZo8FwU2UwEa3+164=
|
||||
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
|
||||
github.com/pion/interceptor v0.1.12 h1:CslaNriCFUItiXS5o+hh5lpL0t0ytQkFnUcbbCs2Zq8=
|
||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
|
||||
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
|
||||
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
|
||||
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
|
||||
github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.7.9/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.7.11 h1:WosqH088pRIAnAoAGZjagA1H3uFtzjyD5yagQXqZ3uo=
|
||||
github.com/pion/rtp v1.7.11/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
|
||||
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
|
||||
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
|
||||
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
|
||||
github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
|
||||
github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
|
||||
github.com/pion/rtcp v1.2.10 h1:nkr3uj+8Sp97zyItdN60tE/S6vk4al5CPRR6Gejsdjc=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/sctp v1.8.5 h1:JCc25nghnXWOlSn3OVtEnA9PjQ2JsxQbG+CXZ1UkJKQ=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
|
||||
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
|
||||
github.com/pion/srtp/v2 v2.0.11 h1:6cEEgT1oCLWgE+BynbfaSMAxtsqU0M096x9dNH6olY0=
|
||||
github.com/pion/srtp/v2 v2.0.11/go.mod h1:vzHprzbuVoYJ9NfaRMycnFrkHcLSaLVuBZDOtFQNZjY=
|
||||
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
|
||||
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
|
||||
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
|
||||
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
|
||||
github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY=
|
||||
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
|
||||
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
|
||||
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
|
||||
github.com/pion/turn/v2 v2.0.9 h1:jcDPw0Vfd5I4iTc7s0Upfc2aMnyu2lgJ9vV0SUrNC1o=
|
||||
github.com/pion/turn/v2 v2.0.9/go.mod h1:DQlwUwx7hL8Xya6TTAabbd9DdKXTNR96Xf5g5Qqso/M=
|
||||
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
|
||||
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
|
||||
github.com/pion/webrtc/v3 v3.1.27 h1:yQ6TuHKJR/vro3nLZMfPv+WGf7T1/4ItaQeuzIZLXs4=
|
||||
github.com/pion/webrtc/v3 v3.1.27/go.mod h1:hdduI+Rx0cpGvva18j0gKy/Iak611WPyhUIXs5W/FuI=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pion/webrtc/v3 v3.1.50 h1:wLMo1+re4WMZ9Kun9qcGcY+XoHkE3i0CXrrc0sjhVCk=
|
||||
github.com/pion/webrtc/v3 v3.1.50/go.mod h1:y9n09weIXB+sjb9mi0GBBewNxo4TKUQm5qdtT5v3/X4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
|
||||
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
|
||||
github.com/prometheus/common v0.33.0 h1:rHgav/0a6+uYgGdNt3jwz8FNSesO/Hsang3O0T9A5SE=
|
||||
github.com/prometheus/common v0.33.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
|
||||
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/veandco/go-sdl2 v0.4.20 h1:/xEP4SBAcGCo++wKv90mxDDRlVPjZ9HpES82FTd6qkg=
|
||||
github.com/veandco/go-sdl2 v0.4.20/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
|
||||
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
|
||||
github.com/veandco/go-sdl2 v0.4.28 h1:kLXyC0MNbQp6aQcow27Nozaos6XT9j1db7hMm2PPPas=
|
||||
github.com/veandco/go-sdl2 v0.4.28/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM=
|
||||
golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
|
||||
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20220321031419-a8550c1d254a h1:LnH9RNcpPv5Kzi15lXg42lYMPUf0x8CuPv1YnvBWZAg=
|
||||
golang.org/x/image v0.0.0-20220321031419-a8550c1d254a/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
|
||||
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/image v0.2.0 h1:/DcQ0w3VHKCC5p0/P2B0JpAZ9Z++V2KOo2fyU89CXBQ=
|
||||
golang.org/x/image v0.2.0/go.mod h1:la7oBXb9w3YFjBqaAwtynVioc1ZvOnNteUNrifGNmAI=
|
||||
golang.org/x/image v0.3.0 h1:HTDXbdK9bjfSWkPzDJIw89W8CAtfFGduujWs33NLLsg=
|
||||
golang.org/x/image v0.3.0/go.mod h1:fXd9211C/0VTlYuAcOhW8dY/RtEJqODXOWBDpmYBf+A=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/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-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c=
|
||||
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f h1:8w7RhxzTVgUzw/AH/9mUV5q0vMgy40SQRursCcfmkCw=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
|
||||
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
151
pkg/api/api.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type (
|
||||
Stateful struct {
|
||||
Id network.Uid `json:"id"`
|
||||
}
|
||||
Room struct {
|
||||
Rid string `json:"room_id"` // room id
|
||||
}
|
||||
StatefulRoom struct {
|
||||
Stateful
|
||||
Room
|
||||
}
|
||||
PT uint8
|
||||
)
|
||||
|
||||
type (
|
||||
RoomInterface interface {
|
||||
GetRoom() string
|
||||
}
|
||||
)
|
||||
|
||||
func StateRoom(id network.Uid, rid string) StatefulRoom {
|
||||
return StatefulRoom{Stateful: Stateful{id}, Room: Room{rid}}
|
||||
}
|
||||
func (sr StatefulRoom) GetRoom() string { return sr.Rid }
|
||||
|
||||
// Packet codes:
|
||||
//
|
||||
// x, 1xx - user codes
|
||||
// 2xx - worker codes
|
||||
const (
|
||||
CheckLatency PT = 3
|
||||
InitSession PT = 4
|
||||
WebrtcInit PT = 100
|
||||
WebrtcOffer PT = 101
|
||||
WebrtcAnswer PT = 102
|
||||
WebrtcIce PT = 103
|
||||
StartGame PT = 104
|
||||
ChangePlayer PT = 108
|
||||
QuitGame PT = 105
|
||||
SaveGame PT = 106
|
||||
LoadGame PT = 107
|
||||
ToggleMultitap PT = 109
|
||||
RecordGame PT = 110
|
||||
GetWorkerList PT = 111
|
||||
RegisterRoom PT = 201
|
||||
CloseRoom PT = 202
|
||||
IceCandidate = WebrtcIce
|
||||
TerminateSession PT = 204
|
||||
)
|
||||
|
||||
func (p PT) String() string {
|
||||
switch p {
|
||||
case CheckLatency:
|
||||
return "CheckLatency"
|
||||
case InitSession:
|
||||
return "InitSession"
|
||||
case WebrtcInit:
|
||||
return "WebrtcInit"
|
||||
case WebrtcOffer:
|
||||
return "WebrtcOffer"
|
||||
case WebrtcAnswer:
|
||||
return "WebrtcAnswer"
|
||||
case WebrtcIce:
|
||||
return "WebrtcIce"
|
||||
case StartGame:
|
||||
return "StartGame"
|
||||
case ChangePlayer:
|
||||
return "ChangePlayer"
|
||||
case QuitGame:
|
||||
return "QuitGame"
|
||||
case SaveGame:
|
||||
return "SaveGame"
|
||||
case LoadGame:
|
||||
return "LoadGame"
|
||||
case ToggleMultitap:
|
||||
return "ToggleMultitap"
|
||||
case RecordGame:
|
||||
return "RecordGame"
|
||||
case GetWorkerList:
|
||||
return "GetWorkerList"
|
||||
case RegisterRoom:
|
||||
return "RegisterRoom"
|
||||
case CloseRoom:
|
||||
return "CloseRoom"
|
||||
case TerminateSession:
|
||||
return "TerminateSession"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Various codes
|
||||
const (
|
||||
EMPTY = ""
|
||||
OK = "ok"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrForbidden = fmt.Errorf("forbidden")
|
||||
ErrMalformed = fmt.Errorf("malformed")
|
||||
)
|
||||
|
||||
func Unwrap[T any](data []byte) *T {
|
||||
out := new(T)
|
||||
if err := json.Unmarshal(data, out); err != nil {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func UnwrapChecked[T any](bytes []byte, err error) (*T, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Unwrap[T](bytes), nil
|
||||
}
|
||||
|
||||
// ToBase64Json encodes data to a URL-encoded Base64+JSON string.
|
||||
func ToBase64Json(data any) (string, error) {
|
||||
if data == nil {
|
||||
return "", nil
|
||||
}
|
||||
b, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
// FromBase64Json decodes data from a URL-encoded Base64+JSON string.
|
||||
func FromBase64Json(data string, obj any) error {
|
||||
b, err := base64.URLEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
47
pkg/api/coordinator.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package api
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
|
||||
type (
|
||||
CloseRoomRequest string
|
||||
ConnectionRequest struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
IsHTTPS bool `json:"is_https,omitempty"`
|
||||
PingURL string `json:"ping_url,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
}
|
||||
GetWorkerListRequest struct{}
|
||||
GetWorkerListResponse struct {
|
||||
Servers []Server `json:"servers"`
|
||||
}
|
||||
RegisterRoomRequest string
|
||||
)
|
||||
|
||||
// Server contains a list of server groups.
|
||||
// Server is a separate machine that may contain
|
||||
// multiple sub-processes.
|
||||
type Server struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Id network.Uid `json:"id,omitempty"`
|
||||
IsBusy bool `json:"is_busy,omitempty"`
|
||||
InGroup bool `json:"in_group,omitempty"`
|
||||
PingURL string `json:"ping_url"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
}
|
||||
|
||||
type HasServerInfo interface {
|
||||
GetServerList() []Server
|
||||
}
|
||||
|
||||
const (
|
||||
DataQueryParam = "data"
|
||||
RoomIdQueryParam = "room_id"
|
||||
ZoneQueryParam = "zone"
|
||||
WorkerIdParam = "wid"
|
||||
)
|
||||
30
pkg/api/user.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package api
|
||||
|
||||
type (
|
||||
ChangePlayerUserRequest int
|
||||
CheckLatencyUserResponse []string
|
||||
CheckLatencyUserRequest map[string]int64
|
||||
GameStartUserRequest struct {
|
||||
GameName string `json:"game_name"`
|
||||
RoomId string `json:"room_id"`
|
||||
Record bool `json:"record,omitempty"`
|
||||
RecordUser string `json:"record_user,omitempty"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
IceServer struct {
|
||||
Urls string `json:"urls,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
InitSessionUserResponse struct {
|
||||
Ice []IceServer `json:"ice"`
|
||||
Games []string `json:"games"`
|
||||
Wid string `json:"wid"`
|
||||
}
|
||||
WebrtcAnswerUserRequest string
|
||||
WebrtcUserIceCandidate string
|
||||
)
|
||||
|
||||
func InitSessionResult(ice []IceServer, games []string, wid string) (PT, InitSessionUserResponse) {
|
||||
return InitSession, InitSessionUserResponse{Ice: ice, Games: games, Wid: wid}
|
||||
}
|
||||
68
pkg/api/worker.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package api
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
|
||||
type GameInfo struct {
|
||||
Name string `json:"name"`
|
||||
Base string `json:"base"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type (
|
||||
ChangePlayerRequest = struct {
|
||||
StatefulRoom
|
||||
Index int `json:"index"`
|
||||
}
|
||||
ChangePlayerResponse int
|
||||
GameQuitRequest struct {
|
||||
StatefulRoom
|
||||
}
|
||||
LoadGameRequest struct {
|
||||
StatefulRoom
|
||||
}
|
||||
LoadGameResponse string
|
||||
SaveGameRequest struct {
|
||||
StatefulRoom
|
||||
}
|
||||
SaveGameResponse string
|
||||
StartGameRequest struct {
|
||||
StatefulRoom
|
||||
Record bool
|
||||
RecordUser string
|
||||
Game GameInfo `json:"game"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
StartGameResponse struct {
|
||||
Room
|
||||
Record bool
|
||||
}
|
||||
RecordGameRequest struct {
|
||||
StatefulRoom
|
||||
Active bool `json:"active"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
RecordGameResponse string
|
||||
TerminateSessionRequest struct {
|
||||
Stateful
|
||||
}
|
||||
ToggleMultitapRequest struct {
|
||||
StatefulRoom
|
||||
}
|
||||
WebrtcAnswerRequest struct {
|
||||
Stateful
|
||||
Sdp string `json:"sdp"`
|
||||
}
|
||||
WebrtcIceCandidateRequest struct {
|
||||
Stateful
|
||||
Candidate string `json:"candidate"`
|
||||
}
|
||||
WebrtcInitRequest struct {
|
||||
Stateful
|
||||
}
|
||||
WebrtcInitResponse string
|
||||
)
|
||||
|
||||
func NewWebrtcIceCandidateRequest(id network.Uid, can string) (PT, any) {
|
||||
return WebrtcIce, WebrtcIceCandidateRequest{Stateful: Stateful{id}, Candidate: can}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
package codec
|
||||
|
||||
type VideoCodec string
|
||||
|
||||
const (
|
||||
H264 VideoCodec = "h264"
|
||||
VPX VideoCodec = "vpx"
|
||||
)
|
||||
93
pkg/com/com.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
)
|
||||
|
||||
type (
|
||||
In struct {
|
||||
Id network.Uid `json:"id,omitempty"`
|
||||
T api.PT `json:"t"`
|
||||
Payload json.RawMessage `json:"p,omitempty"`
|
||||
}
|
||||
Out struct {
|
||||
Id network.Uid `json:"id,omitempty"`
|
||||
T api.PT `json:"t"`
|
||||
Payload any `json:"p,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
EmptyPacket = Out{Payload: ""}
|
||||
ErrPacket = Out{Payload: "err"}
|
||||
OkPacket = Out{Payload: "ok"}
|
||||
)
|
||||
|
||||
type (
|
||||
NetClient interface {
|
||||
Close()
|
||||
Id() network.Uid
|
||||
}
|
||||
RegionalClient interface {
|
||||
In(region string) bool
|
||||
}
|
||||
)
|
||||
|
||||
type SocketClient struct {
|
||||
NetClient
|
||||
|
||||
id network.Uid
|
||||
wire *Client
|
||||
Tag string
|
||||
Log *logger.Logger
|
||||
}
|
||||
|
||||
func New(conn *Client, tag string, id network.Uid, log *logger.Logger) SocketClient {
|
||||
l := log.Extend(log.With().Str("cid", id.Short()))
|
||||
dir := "→"
|
||||
if conn.IsServer() {
|
||||
dir = "←"
|
||||
}
|
||||
l.Info().Str("c", tag).Str("d", dir).Msg("Connect")
|
||||
return SocketClient{id: id, wire: conn, Tag: tag, Log: l}
|
||||
}
|
||||
|
||||
func (c *SocketClient) SetId(id network.Uid) { c.id = id }
|
||||
|
||||
func (c *SocketClient) OnPacket(fn func(p In) error) {
|
||||
logFn := func(p In) {
|
||||
c.Log.Info().Str("c", c.Tag).Str("d", "←").Msgf("%s", p.T)
|
||||
if err := fn(p); err != nil {
|
||||
c.Log.Error().Err(err).Send()
|
||||
}
|
||||
}
|
||||
c.wire.OnPacket(logFn)
|
||||
}
|
||||
|
||||
// Send makes a blocking call.
|
||||
func (c *SocketClient) Send(t api.PT, data any) ([]byte, error) {
|
||||
c.Log.Info().Str("c", c.Tag).Str("d", "→").Msgf("ᵇ%s", t)
|
||||
return c.wire.Call(t, data)
|
||||
}
|
||||
|
||||
// Notify just sends a message and goes further.
|
||||
func (c *SocketClient) Notify(t api.PT, data any) {
|
||||
c.Log.Info().Str("c", c.Tag).Str("d", "→").Msgf("%s", t)
|
||||
_ = c.wire.Send(t, data)
|
||||
}
|
||||
|
||||
func (c *SocketClient) Close() {
|
||||
c.wire.Close()
|
||||
c.Log.Info().Str("c", c.Tag).Str("d", "x").Msg("Close")
|
||||
}
|
||||
|
||||
func (c *SocketClient) Id() network.Uid { return c.id }
|
||||
func (c *SocketClient) Listen() { c.ProcessMessages(); <-c.Done() }
|
||||
func (c *SocketClient) ProcessMessages() { c.wire.Listen() }
|
||||
func (c *SocketClient) Route(in In, out Out) { _ = c.wire.Route(in, out) }
|
||||
func (c *SocketClient) String() string { return c.Tag + ":" + string(c.Id()) }
|
||||
func (c *SocketClient) Done() chan struct{} { return c.wire.Wait() }
|
||||
98
pkg/com/map.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
)
|
||||
|
||||
// NetMap defines a thread-safe NetClient list.
|
||||
type NetMap[T NetClient] struct {
|
||||
m map[string]T
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// ErrNotFound is returned by NetMap when some value is not present.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
func NewNetMap[T NetClient]() NetMap[T] { return NetMap[T]{m: make(map[string]T, 10)} }
|
||||
|
||||
// Add adds a new NetClient value with its id value as the key.
|
||||
func (m *NetMap[T]) Add(client T) { m.Put(string(client.Id()), client) }
|
||||
|
||||
// Put adds a new NetClient value with a custom key value.
|
||||
func (m *NetMap[T]) Put(key string, client T) {
|
||||
m.mu.Lock()
|
||||
m.m[key] = client
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// Remove removes NetClient from the map if present.
|
||||
func (m *NetMap[T]) Remove(client T) { m.RemoveByKey(string(client.Id())) }
|
||||
|
||||
// RemoveByKey removes NetClient from the map by a specified key value.
|
||||
func (m *NetMap[T]) RemoveByKey(key string) {
|
||||
m.mu.Lock()
|
||||
delete(m.m, key)
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
// RemoveAll removes all occurrences of specified NetClient.
|
||||
func (m *NetMap[T]) RemoveAll(client T) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for k, c := range m.m {
|
||||
if c.Id() == client.Id() {
|
||||
delete(m.m, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *NetMap[T]) IsEmpty() bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.m) == 0
|
||||
}
|
||||
|
||||
// List returns the current NetClient map.
|
||||
func (m *NetMap[T]) List() map[string]T { return m.m }
|
||||
|
||||
func (m *NetMap[T]) Has(id network.Uid) bool {
|
||||
_, err := m.Find(string(id))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Find searches the first NetClient by a specified key value.
|
||||
func (m *NetMap[T]) Find(key string) (client T, err error) {
|
||||
if key == "" {
|
||||
return client, ErrNotFound
|
||||
}
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if c, ok := m.m[key]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return client, ErrNotFound
|
||||
}
|
||||
|
||||
// FindBy searches the first NetClient with the provided predicate function.
|
||||
func (m *NetMap[T]) FindBy(fn func(c T) bool) (client T, err error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, w := range m.m {
|
||||
if fn(w) {
|
||||
return w, nil
|
||||
}
|
||||
}
|
||||
return client, ErrNotFound
|
||||
}
|
||||
|
||||
// ForEach processes every NetClient with the provided callback function.
|
||||
func (m *NetMap[T]) ForEach(fn func(c T)) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, w := range m.m {
|
||||
fn(w)
|
||||
}
|
||||
}
|
||||
32
pkg/com/map_test.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
)
|
||||
|
||||
type testClient struct {
|
||||
NetClient
|
||||
id int
|
||||
c int32
|
||||
}
|
||||
|
||||
func (t *testClient) Id() network.Uid { return network.Uid(fmt.Sprintf("%v", t.id)) }
|
||||
func (t *testClient) change(n int) { atomic.AddInt32(&t.c, int32(n)) }
|
||||
|
||||
func TestPointerValue(t *testing.T) {
|
||||
m := NewNetMap[*testClient]()
|
||||
c := testClient{id: 1}
|
||||
m.Add(&c)
|
||||
fc, _ := m.FindBy(func(c *testClient) bool { return c.id == 1 })
|
||||
c.change(100)
|
||||
fc2, _ := m.Find(fc.Id().String())
|
||||
|
||||
expected := c.c == fc.c && c.c == fc2.c
|
||||
if !expected {
|
||||
t.Errorf("not expected change, o: %v != %v != %v", c.c, fc.c, fc2.c)
|
||||
}
|
||||
}
|
||||
187
pkg/com/net.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network/websocket"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type (
|
||||
Connector struct {
|
||||
tag string
|
||||
wu *websocket.Upgrader
|
||||
}
|
||||
Client struct {
|
||||
conn *websocket.WS
|
||||
queue map[network.Uid]*call
|
||||
onPacket func(packet In)
|
||||
mu sync.Mutex
|
||||
}
|
||||
call struct {
|
||||
done chan struct{}
|
||||
err error
|
||||
Response In
|
||||
}
|
||||
Option = func(c *Connector)
|
||||
)
|
||||
|
||||
var (
|
||||
errConnClosed = errors.New("connection closed")
|
||||
errTimeout = errors.New("timeout")
|
||||
)
|
||||
var outPool = sync.Pool{New: func() any { o := Out{}; return &o }}
|
||||
|
||||
func WithOrigin(url string) Option { return func(c *Connector) { c.wu = websocket.NewUpgrader(url) } }
|
||||
func WithTag(tag string) Option { return func(c *Connector) { c.tag = tag } }
|
||||
|
||||
const callTimeout = 5 * time.Second
|
||||
|
||||
func NewConnector(opts ...Option) *Connector {
|
||||
c := &Connector{}
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
if c.wu == nil {
|
||||
c.wu = &websocket.DefaultUpgrader
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (co *Connector) NewClientServer(w http.ResponseWriter, r *http.Request, log *logger.Logger) (*SocketClient, error) {
|
||||
ws, err := co.wu.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := connect(websocket.NewServerWithConn(ws, log))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := New(conn, co.tag, network.NewUid(), log)
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
func (co *Connector) NewClient(address url.URL, log *logger.Logger) (*Client, error) {
|
||||
return connect(websocket.NewClient(address, log))
|
||||
}
|
||||
|
||||
func connect(conn *websocket.WS, err error) (*Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &Client{conn: conn, queue: make(map[network.Uid]*call, 1)}
|
||||
client.conn.OnMessage = client.handleMessage
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (c *Client) IsServer() bool { return c.conn.IsServer() }
|
||||
|
||||
func (c *Client) OnPacket(fn func(packet In)) { c.mu.Lock(); c.onPacket = fn; c.mu.Unlock() }
|
||||
|
||||
func (c *Client) Listen() { c.mu.Lock(); c.conn.Listen(); c.mu.Unlock() }
|
||||
|
||||
func (c *Client) Close() {
|
||||
// !to handle error
|
||||
c.conn.Close()
|
||||
c.drain(errConnClosed)
|
||||
}
|
||||
|
||||
func (c *Client) Call(type_ api.PT, payload any) ([]byte, error) {
|
||||
// !to expose channel instead of results
|
||||
rq := outPool.Get().(*Out)
|
||||
rq.Id, rq.T, rq.Payload = network.NewUid(), type_, payload
|
||||
r, err := json.Marshal(rq)
|
||||
outPool.Put(rq)
|
||||
if err != nil {
|
||||
//delete(c.queue, id)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := &call{done: make(chan struct{})}
|
||||
c.mu.Lock()
|
||||
c.queue[rq.Id] = task
|
||||
c.conn.Write(r)
|
||||
c.mu.Unlock()
|
||||
select {
|
||||
case <-task.done:
|
||||
case <-time.After(callTimeout):
|
||||
task.err = errTimeout
|
||||
}
|
||||
return task.Response.Payload, task.err
|
||||
}
|
||||
|
||||
func (c *Client) Send(type_ api.PT, pl any) error {
|
||||
rq := outPool.Get().(*Out)
|
||||
rq.Id, rq.T, rq.Payload = "", type_, pl
|
||||
defer outPool.Put(rq)
|
||||
return c.SendPacket(rq)
|
||||
}
|
||||
|
||||
func (c *Client) Route(p In, pl Out) error {
|
||||
rq := outPool.Get().(*Out)
|
||||
rq.Id, rq.T, rq.Payload = p.Id, p.T, pl.Payload
|
||||
defer outPool.Put(rq)
|
||||
return c.SendPacket(rq)
|
||||
}
|
||||
|
||||
func (c *Client) SendPacket(packet *Out) error {
|
||||
r, err := json.Marshal(packet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.conn.Write(r)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) Wait() chan struct{} { return c.conn.Done }
|
||||
|
||||
func (c *Client) handleMessage(message []byte, err error) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var res In
|
||||
if err = json.Unmarshal(message, &res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// empty id implies that we won't track (wait) the response
|
||||
if !res.Id.Empty() {
|
||||
if task := c.pop(res.Id); task != nil {
|
||||
task.Response = res
|
||||
close(task.done)
|
||||
return
|
||||
}
|
||||
}
|
||||
c.onPacket(res)
|
||||
}
|
||||
|
||||
// pop extracts and removes a task from the queue by its id.
|
||||
func (c *Client) pop(id network.Uid) *call {
|
||||
c.mu.Lock()
|
||||
task := c.queue[id]
|
||||
delete(c.queue, id)
|
||||
c.mu.Unlock()
|
||||
return task
|
||||
}
|
||||
|
||||
// drain cancels all what's left in the task queue.
|
||||
func (c *Client) drain(err error) {
|
||||
c.mu.Lock()
|
||||
for _, task := range c.queue {
|
||||
if task.err == nil {
|
||||
task.err = err
|
||||
}
|
||||
close(task.done)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
171
pkg/com/net_test.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package com
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network/websocket"
|
||||
)
|
||||
|
||||
var log = logger.Default()
|
||||
|
||||
func TestPackets(t *testing.T) {
|
||||
r, err := json.Marshal(Out{Payload: "asd"})
|
||||
if err != nil {
|
||||
t.Fatalf("can't marshal packet")
|
||||
}
|
||||
|
||||
t.Logf("PACKET: %v", string(r))
|
||||
}
|
||||
|
||||
func TestWebsocket(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
test func(t *testing.T)
|
||||
}{
|
||||
{"If WebSocket implementation is OK in general", testWebsocket},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, tc.test)
|
||||
}
|
||||
}
|
||||
|
||||
func testWebsocket(t *testing.T) {
|
||||
// setup
|
||||
// socket handler
|
||||
var socket *websocket.WS
|
||||
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := websocket.DefaultUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("no socket, %v", err)
|
||||
}
|
||||
sock, err := websocket.NewServerWithConn(conn, log)
|
||||
if err != nil {
|
||||
t.Fatalf("couldn't init socket server")
|
||||
}
|
||||
socket = sock
|
||||
socket.OnMessage = func(message []byte, err error) {
|
||||
// echo response
|
||||
socket.Write(message)
|
||||
}
|
||||
socket.Listen()
|
||||
})
|
||||
// http handler
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Done()
|
||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
t.Errorf("no server")
|
||||
return
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
client := newClient(t, url.URL{Scheme: "ws", Host: "localhost:8080", Path: "/ws"})
|
||||
client.Listen()
|
||||
|
||||
calls := []struct {
|
||||
typ api.PT
|
||||
payload any
|
||||
concurrent bool
|
||||
value any
|
||||
}{
|
||||
{typ: 10, payload: "test", value: "test", concurrent: true},
|
||||
{typ: 10, payload: "test2", value: "test2"},
|
||||
{typ: 11, payload: "test3", value: "test3"},
|
||||
{typ: 99, payload: "", value: ""},
|
||||
{typ: 0},
|
||||
{typ: 12, payload: 123, value: 123},
|
||||
{typ: 10, payload: false, value: false},
|
||||
{typ: 10, payload: true, value: true},
|
||||
{typ: 11, payload: []string{"test", "test", "test"}, value: []string{"test", "test", "test"}},
|
||||
{typ: 22, payload: []string{}, value: []string{}},
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
n := 42 * 2 * 2
|
||||
var wait sync.WaitGroup
|
||||
wait.Add(n * len(calls))
|
||||
|
||||
// test
|
||||
for _, call := range calls {
|
||||
for i := 0; i < n; i++ {
|
||||
if call.concurrent {
|
||||
call := call
|
||||
go func() {
|
||||
w := rand.Intn(600-100) + 100
|
||||
time.Sleep(time.Duration(w) * time.Millisecond)
|
||||
vv, err := client.Call(call.typ, call.payload)
|
||||
checkCall(t, vv, err, call.value)
|
||||
wait.Done()
|
||||
}()
|
||||
} else {
|
||||
vv, err := client.Call(call.typ, call.payload)
|
||||
checkCall(t, vv, err, call.value)
|
||||
wait.Done()
|
||||
}
|
||||
}
|
||||
}
|
||||
wait.Wait()
|
||||
|
||||
client.Close()
|
||||
|
||||
<-socket.Done
|
||||
<-client.conn.Done
|
||||
}
|
||||
|
||||
func newClient(t *testing.T, addr url.URL) *Client {
|
||||
conn, err := NewConnector().NewClient(addr, log)
|
||||
if err != nil {
|
||||
t.Fatalf("error: couldn't connect to %v because of %v", addr.String(), err)
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func checkCall(t *testing.T, v []byte, err error, need any) {
|
||||
if err != nil {
|
||||
t.Fatalf("should be no error but %v", err)
|
||||
return
|
||||
}
|
||||
var value any
|
||||
if v != nil {
|
||||
if err = json.Unmarshal(v, &value); err != nil {
|
||||
t.Fatalf("can't unmarshal %v", v)
|
||||
}
|
||||
}
|
||||
|
||||
nice := true
|
||||
// cast values after default unmarshal
|
||||
switch value.(type) {
|
||||
default:
|
||||
nice = value == need
|
||||
case bool:
|
||||
nice = value == need.(bool)
|
||||
case float64:
|
||||
nice = value == float64(need.(int))
|
||||
case []any:
|
||||
// let's assume that's strings
|
||||
vv := value.([]any)
|
||||
for i := 0; i < len(need.([]string)); i++ {
|
||||
if vv[i].(string) != need.([]string)[i] {
|
||||
nice = false
|
||||
break
|
||||
}
|
||||
}
|
||||
case map[string]any:
|
||||
// ???
|
||||
}
|
||||
|
||||
if !nice {
|
||||
t.Fatalf("expected %v is not expected %v", need, v)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +1,35 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/emulator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/monitoring"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/shared"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Coordinator struct {
|
||||
Debug bool
|
||||
Library games.Config
|
||||
Monitoring monitoring.Config
|
||||
Origin struct {
|
||||
UserWs string
|
||||
WorkerWs string
|
||||
}
|
||||
Server shared.Server
|
||||
Analytics Analytics
|
||||
Coordinator Coordinator
|
||||
Emulator emulator.Emulator
|
||||
Recording shared.Recording
|
||||
Version shared.Version
|
||||
Webrtc webrtc.Webrtc
|
||||
}
|
||||
|
||||
type Coordinator struct {
|
||||
Analytics Analytics
|
||||
Debug bool
|
||||
Library games.Config
|
||||
Monitoring monitoring.Config
|
||||
Origin struct {
|
||||
UserWs string
|
||||
WorkerWs string
|
||||
}
|
||||
Emulator emulator.Emulator
|
||||
Recording shared.Recording
|
||||
Webrtc webrtc.Webrtc
|
||||
Selector string
|
||||
Server shared.Server
|
||||
}
|
||||
|
||||
// Analytics is optional Google Analytics
|
||||
|
|
@ -33,6 +38,11 @@ type Analytics struct {
|
|||
Gtag string
|
||||
}
|
||||
|
||||
const (
|
||||
SelectAny = "any"
|
||||
SelectByPing = "ping"
|
||||
)
|
||||
|
||||
// allows custom config path
|
||||
var configPath string
|
||||
|
||||
|
|
@ -48,6 +58,6 @@ func NewConfig() (conf Config) {
|
|||
func (c *Config) ParseFlags() {
|
||||
c.Coordinator.Server.WithFlags()
|
||||
flag.IntVar(&c.Coordinator.Monitoring.Port, "monitoring.port", c.Coordinator.Monitoring.Port, "Monitoring server port")
|
||||
flag.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path")
|
||||
flag.StringVar(&configPath, "c-conf", configPath, "Set custom configuration file path")
|
||||
flag.Parse()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package emulator
|
||||
|
||||
import (
|
||||
"math"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -9,16 +10,30 @@ import (
|
|||
type Emulator struct {
|
||||
Scale int
|
||||
Threads int
|
||||
AspectRatio struct {
|
||||
Keep bool
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
AspectRatio AspectRatio
|
||||
Storage string
|
||||
LocalPath string
|
||||
Libretro LibretroConfig
|
||||
AutosaveSec int
|
||||
}
|
||||
|
||||
type AspectRatio struct {
|
||||
Keep bool
|
||||
Width int
|
||||
Height int
|
||||
}
|
||||
|
||||
func (a AspectRatio) ResizeToAspect(ratio float64, sw int, sh int) (dw int, dh int) {
|
||||
// ratio is always > 0
|
||||
dw = int(math.Round(float64(sh)*ratio/2) * 2)
|
||||
dh = sh
|
||||
if dw > sw {
|
||||
dw = sw
|
||||
dh = int(math.Round(float64(sw)/ratio/2) * 2)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type LibretroConfig struct {
|
||||
Cores struct {
|
||||
Paths struct {
|
||||
|
|
@ -34,6 +49,7 @@ type LibretroConfig struct {
|
|||
List map[string]LibretroCoreConfig
|
||||
}
|
||||
SaveCompression bool
|
||||
LogLevel int
|
||||
}
|
||||
|
||||
type LibretroRepoConfig struct {
|
||||
|
|
@ -49,7 +65,6 @@ type LibretroCoreConfig struct {
|
|||
Folder string
|
||||
Width int
|
||||
Height int
|
||||
Ratio float64
|
||||
IsGlAllowed bool
|
||||
UsesLibCo bool
|
||||
HasMultitap bool
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
package encoder
|
||||
|
||||
type Encoder struct {
|
||||
Audio Audio
|
||||
Video Video
|
||||
WithoutGame bool
|
||||
Audio Audio
|
||||
Video Video
|
||||
}
|
||||
|
||||
type Audio struct {
|
||||
Channels int
|
||||
Frame int
|
||||
Frequency int
|
||||
Frame int
|
||||
}
|
||||
|
||||
type Video struct {
|
||||
Codec string
|
||||
H264 struct {
|
||||
Codec string
|
||||
Concurrency int
|
||||
H264 struct {
|
||||
Crf uint8
|
||||
Preset string
|
||||
Profile string
|
||||
|
|
@ -26,6 +24,3 @@ type Video struct {
|
|||
KeyframeInterval uint
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Audio) GetFrameSize() int { return a.GetFrameSizeFor(a.Frequency) }
|
||||
func (a *Audio) GetFrameSizeFor(hz int) int { return hz * a.Frame / 1000 * a.Channels }
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const EnvPrefix = "CLOUD_GAME"
|
|||
// The path param specifies a custom path to the configuration file.
|
||||
// Reads and puts environment variables with the prefix CLOUD_GAME_.
|
||||
// Params from the config should be in uppercase separated with _.
|
||||
func LoadConfig(config interface{}, path string) error {
|
||||
func LoadConfig(config any, path string) error {
|
||||
dirs := []string{path}
|
||||
if path == "" {
|
||||
dirs = append(dirs, ".", "configs", "../../../configs")
|
||||
|
|
@ -26,6 +26,6 @@ func LoadConfig(config interface{}, path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func LoadConfigEnv(config interface{}) error {
|
||||
func LoadConfigEnv(config any) error {
|
||||
return fig.Load(config, fig.IgnoreFile(), fig.UseEnv(EnvPrefix))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package shared
|
||||
|
||||
import flag "github.com/spf13/pflag"
|
||||
import "flag"
|
||||
|
||||
type Version int
|
||||
|
||||
type Server struct {
|
||||
Address string
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/encoder"
|
||||
)
|
||||
|
||||
type Webrtc struct {
|
||||
|
|
@ -19,27 +18,28 @@ type Webrtc struct {
|
|||
IceIpMap string
|
||||
IceLite bool
|
||||
SinglePort int
|
||||
LogLevel int
|
||||
}
|
||||
|
||||
type IceServer struct {
|
||||
Url string
|
||||
Username string
|
||||
Credential string
|
||||
Urls string `json:"urls,omitempty"`
|
||||
Username string `json:"username,omitempty"`
|
||||
Credential string `json:"credential,omitempty"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Encoder encoder.Encoder
|
||||
Webrtc Webrtc
|
||||
}
|
||||
func (w *Webrtc) HasDtlsRole() bool { return w.DtlsRole > 0 }
|
||||
func (w *Webrtc) HasPortRange() bool { return w.IcePorts.Min > 0 && w.IcePorts.Max > 0 }
|
||||
func (w *Webrtc) HasSinglePort() bool { return w.SinglePort > 0 }
|
||||
func (w *Webrtc) HasIceIpMap() bool { return w.IceIpMap != "" }
|
||||
|
||||
func (w *Webrtc) AddIceServersEnv() {
|
||||
cfg := Config{Webrtc: Webrtc{IceServers: []IceServer{{}, {}, {}, {}, {}}}}
|
||||
cfg := Webrtc{IceServers: []IceServer{{}, {}, {}, {}, {}}}
|
||||
_ = config.LoadConfigEnv(&cfg)
|
||||
for i, ice := range cfg.Webrtc.IceServers {
|
||||
if ice.Url == "" {
|
||||
for i, ice := range cfg.IceServers {
|
||||
if ice.Urls == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(ice.Url, "turn:") || strings.HasPrefix(ice.Url, "turns:") {
|
||||
if strings.HasPrefix(ice.Urls, "turn:") || strings.HasPrefix(ice.Urls, "turns:") {
|
||||
if ice.Username == "" || ice.Credential == "" {
|
||||
log.Fatalf("TURN or TURNS servers should have both username and credential: %+v", ice)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package worker
|
||||
|
||||
import (
|
||||
"log"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config"
|
||||
|
|
@ -14,7 +16,6 @@ import (
|
|||
"github.com/giongto35/cloud-game/v2/pkg/config/storage"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/os"
|
||||
flag "github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
|
|
@ -24,9 +25,11 @@ type Config struct {
|
|||
Storage storage.Storage
|
||||
Worker Worker
|
||||
Webrtc webrtc.Webrtc
|
||||
Version shared.Version
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
Debug bool
|
||||
Monitoring monitoring.Config
|
||||
Network struct {
|
||||
CoordinatorAddress string
|
||||
|
|
@ -44,14 +47,12 @@ type Worker struct {
|
|||
var configPath string
|
||||
|
||||
func NewConfig() (conf Config) {
|
||||
_ = config.LoadConfig(&conf, configPath)
|
||||
conf.expandSpecialTags()
|
||||
// with ICE lite we clear ICE servers
|
||||
if !conf.Webrtc.IceLite {
|
||||
conf.Webrtc.AddIceServersEnv()
|
||||
} else {
|
||||
conf.Webrtc.IceServers = []webrtc.IceServer{}
|
||||
err := config.LoadConfig(&conf, configPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
conf.expandSpecialTags()
|
||||
conf.fixValues()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +64,7 @@ func (c *Config) ParseFlags() {
|
|||
flag.IntVar(&c.Worker.Monitoring.Port, "monitoring.port", c.Worker.Monitoring.Port, "Monitoring server port")
|
||||
flag.StringVar(&c.Worker.Network.CoordinatorAddress, "coordinatorhost", c.Worker.Network.CoordinatorAddress, "Worker URL to connect")
|
||||
flag.StringVar(&c.Worker.Network.Zone, "zone", c.Worker.Network.Zone, "Worker network zone (us, eu, etc.)")
|
||||
flag.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path")
|
||||
flag.StringVar(&configPath, "w-conf", configPath, "Set custom configuration file path")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
|
|
@ -76,9 +77,20 @@ func (c *Config) expandSpecialTags() {
|
|||
}
|
||||
userHomeDir, err := os.GetUserHome()
|
||||
if err != nil {
|
||||
log.Fatalln("couldn't read user home directory", err)
|
||||
panic(fmt.Sprintf("couldn't read user home directory, %v", err))
|
||||
}
|
||||
*dir = strings.Replace(*dir, tag, userHomeDir, -1)
|
||||
*dir = filepath.FromSlash(*dir)
|
||||
}
|
||||
}
|
||||
|
||||
// fixValues tries to fix some values otherwise hard to set externally.
|
||||
func (c *Config) fixValues() {
|
||||
// with ICE lite we clear ICE servers
|
||||
if !c.Webrtc.IceLite {
|
||||
c.Webrtc.AddIceServersEnv()
|
||||
} else {
|
||||
c.Webrtc.IceServers = []webrtc.IceServer{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
105
pkg/coordinator/balancer.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
func (h *Hub) findWorkerByRoom(id string, region string) *Worker {
|
||||
if id == "" {
|
||||
return nil
|
||||
}
|
||||
// if there is zone param, we need to ensure the worker in that zone,
|
||||
// if not we consider the room is missing
|
||||
w, _ := h.workers.FindBy(func(w *Worker) bool { return w.RoomId == id && w.In(region) })
|
||||
return w
|
||||
}
|
||||
|
||||
func (h *Hub) getAvailableWorkers(region string) []*Worker {
|
||||
var workers []*Worker
|
||||
h.workers.ForEach(func(w *Worker) {
|
||||
if w.HasSlot() && w.In(region) {
|
||||
workers = append(workers, w)
|
||||
}
|
||||
})
|
||||
return workers
|
||||
}
|
||||
|
||||
func (h *Hub) find1stFreeWorker(region string) *Worker {
|
||||
workers := h.getAvailableWorkers(region)
|
||||
if len(workers) > 0 {
|
||||
return workers[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findFastestWorker returns the best server for a session.
|
||||
// All workers addresses are sent to user and user will ping to get latency.
|
||||
// !to rewrite
|
||||
func (h *Hub) findFastestWorker(region string, fn func(addresses []string) (map[string]int64, error)) *Worker {
|
||||
workers := h.getAvailableWorkers(region)
|
||||
if len(workers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var addresses []string
|
||||
group := map[string][]struct{}{}
|
||||
for _, w := range workers {
|
||||
if _, ok := group[w.PingServer]; !ok {
|
||||
addresses = append(addresses, w.PingServer)
|
||||
}
|
||||
group[w.PingServer] = append(group[w.PingServer], struct{}{})
|
||||
}
|
||||
|
||||
latencies, err := fn(addresses)
|
||||
if len(latencies) == 0 || err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
workers = h.getAvailableWorkers(region)
|
||||
if len(workers) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var bestWorker *Worker
|
||||
var minLatency int64 = 1<<31 - 1
|
||||
// get a worker with the lowest latency
|
||||
for addr, ping := range latencies {
|
||||
if ping < minLatency {
|
||||
for _, w := range workers {
|
||||
if w.PingServer == addr {
|
||||
bestWorker = w
|
||||
}
|
||||
}
|
||||
minLatency = ping
|
||||
}
|
||||
}
|
||||
return bestWorker
|
||||
}
|
||||
|
||||
func (h *Hub) findWorkerById(workerId string, useAllWorkers bool) *Worker {
|
||||
// when we select one particular worker
|
||||
if workerId != "" {
|
||||
if xid_, err := xid.FromString(workerId); err == nil {
|
||||
if useAllWorkers {
|
||||
for _, w := range h.getAvailableWorkers("") {
|
||||
if xid_.String() == w.Id().String() {
|
||||
return w
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, w := range h.getAvailableWorkers("") {
|
||||
xid__, err := xid.FromString(workerId)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bytes.Equal(xid_.Machine(), xid__.Machine()) {
|
||||
return w
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type BrowserClient struct {
|
||||
*cws.Client
|
||||
SessionID string
|
||||
RoomID string
|
||||
WorkerID string // TODO: how about pointer to workerClient?
|
||||
}
|
||||
|
||||
// NewCoordinatorClient returns a client connecting to browser.
|
||||
// This connection exchanges information between browser and coordinator.
|
||||
func NewBrowserClient(c *websocket.Conn, browserID string) *BrowserClient {
|
||||
return &BrowserClient{
|
||||
Client: cws.NewClient(c),
|
||||
SessionID: browserID,
|
||||
}
|
||||
}
|
||||
|
||||
// Register new log
|
||||
func (bc *BrowserClient) Printf(format string, args ...interface{}) {
|
||||
log.Printf(fmt.Sprintf("Browser %s] %s", bc.SessionID, format), args...)
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) Println(args ...interface{}) {
|
||||
log.Println(fmt.Sprintf("Browser %s] %s", bc.SessionID, fmt.Sprint(args...)))
|
||||
}
|
||||
|
|
@ -1,27 +1,83 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"log"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/shared"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/monitoring"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network/httpx"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/service"
|
||||
)
|
||||
|
||||
func New(conf coordinator.Config) (services service.Group) {
|
||||
srv := NewServer(conf, games.NewLibWhitelisted(conf.Coordinator.Library, conf.Emulator))
|
||||
httpSrv, err := NewHTTPServer(conf, func(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/ws", srv.WS)
|
||||
mux.HandleFunc("/wso", srv.WSO)
|
||||
func New(conf coordinator.Config, log *logger.Logger) (services service.Group) {
|
||||
lib := games.NewLibWhitelisted(conf.Coordinator.Library, conf.Emulator, log)
|
||||
lib.Scan()
|
||||
hub := NewHub(conf, lib, log)
|
||||
h, err := NewHTTPServer(conf, log, func(mux *httpx.Mux) *httpx.Mux {
|
||||
mux.HandleFunc("/ws", hub.handleUserConnection)
|
||||
mux.HandleFunc("/wso", hub.handleWorkerConnection)
|
||||
return mux
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("http init fail: %v", err)
|
||||
log.Error().Err(err).Msg("http server init fail")
|
||||
return
|
||||
}
|
||||
services.Add(srv, httpSrv)
|
||||
services.Add(hub, h)
|
||||
if conf.Coordinator.Monitoring.IsEnabled() {
|
||||
services.Add(monitoring.New(conf.Coordinator.Monitoring, httpSrv.GetHost(), "cord"))
|
||||
services.Add(monitoring.New(conf.Coordinator.Monitoring, h.GetHost(), log))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func NewHTTPServer(conf coordinator.Config, log *logger.Logger, fnMux func(*httpx.Mux) *httpx.Mux) (*httpx.Server, error) {
|
||||
return httpx.NewServer(
|
||||
conf.Coordinator.Server.GetAddr(),
|
||||
func(s *httpx.Server) httpx.Handler {
|
||||
return fnMux(s.Mux().
|
||||
Handle("/", index(conf, log)).
|
||||
Static("/static/", "./web"))
|
||||
},
|
||||
httpx.WithServerConfig(conf.Coordinator.Server),
|
||||
httpx.WithLogger(log),
|
||||
)
|
||||
}
|
||||
|
||||
func index(conf coordinator.Config, log *logger.Logger) httpx.Handler {
|
||||
const indexHTML = "./web/index.html"
|
||||
|
||||
handler := func(tpl *template.Template, w httpx.ResponseWriter, r *httpx.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
httpx.NotFound(w)
|
||||
return
|
||||
}
|
||||
// render index page with some tpl values
|
||||
tplData := struct {
|
||||
Analytics coordinator.Analytics
|
||||
Recording shared.Recording
|
||||
}{conf.Coordinator.Analytics, conf.Recording}
|
||||
if err := tpl.Execute(w, tplData); err != nil {
|
||||
log.Fatal().Err(err).Msg("error with the analytics template file")
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Coordinator.Debug {
|
||||
log.Info().Msgf("Using auto-reloading index.html")
|
||||
return httpx.HandlerFunc(func(w httpx.ResponseWriter, r *httpx.Request) {
|
||||
tpl, _ := template.ParseFiles(indexHTML)
|
||||
handler(tpl, w, r)
|
||||
})
|
||||
}
|
||||
|
||||
indexTpl, err := template.ParseFiles(indexHTML)
|
||||
if err != nil {
|
||||
log.Fatal().Err(err).Msg("error with the HTML index page")
|
||||
}
|
||||
|
||||
return httpx.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
handler(indexTpl, writer, request)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,389 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/rs/xid"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/ice"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network/websocket"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/service"
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
service.Service
|
||||
|
||||
cfg coordinator.Config
|
||||
// games library
|
||||
library games.GameLibrary
|
||||
// roomToWorker map roomID to workerID
|
||||
roomToWorker map[string]string
|
||||
// workerClients are the map workerID to worker Client
|
||||
workerClients map[string]*WorkerClient
|
||||
// browserClients are the map sessionID to browser Client
|
||||
browserClients map[string]*BrowserClient
|
||||
|
||||
userWsUpgrader, workerWsUpgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
func NewServer(cfg coordinator.Config, library games.GameLibrary) *Server {
|
||||
// scan the lib right away
|
||||
library.Scan()
|
||||
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
library: library,
|
||||
// Mapping roomID to server
|
||||
roomToWorker: map[string]string{},
|
||||
// Mapping workerID to worker
|
||||
workerClients: map[string]*WorkerClient{},
|
||||
// Mapping sessionID to browser
|
||||
browserClients: map[string]*BrowserClient{},
|
||||
}
|
||||
|
||||
// a custom Origin check
|
||||
s.workerWsUpgrader = websocket.NewUpgrader(cfg.Coordinator.Origin.WorkerWs)
|
||||
s.userWsUpgrader = websocket.NewUpgrader(cfg.Coordinator.Origin.UserWs)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// WSO handles all connections from a new worker to coordinator
|
||||
func (s *Server) WSO(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Coordinator: A worker is connecting...")
|
||||
|
||||
connRt, err := GetConnectionRequest(r.URL.Query().Get("data"))
|
||||
if err != nil {
|
||||
log.Printf("Coordinator: got a malformed request: %v", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("%v", connRt)
|
||||
|
||||
if connRt.PingURL == "" {
|
||||
log.Printf("Warning! Ping address is not set.")
|
||||
}
|
||||
|
||||
if s.cfg.Coordinator.Server.Https && !connRt.IsHTTPS {
|
||||
log.Printf("Warning! Unsecure connection. The worker may not work properly without HTTPS on its side!")
|
||||
}
|
||||
|
||||
c, err := s.workerWsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Coordinator: [!] WS upgrade:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate workerID
|
||||
var workerID string
|
||||
for {
|
||||
workerID = uuid.Must(uuid.NewV4()).String()
|
||||
// check duplicate
|
||||
if _, ok := s.workerClients[workerID]; !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create a workerClient instance
|
||||
wc := NewWorkerClient(c, workerID)
|
||||
wc.Println("Generated worker ID")
|
||||
if connRt.Xid != "" {
|
||||
if wc.Id, err = xid.FromString(connRt.Xid); err != nil {
|
||||
wc.Id = xid.New()
|
||||
}
|
||||
} else {
|
||||
wc.Id = xid.New()
|
||||
}
|
||||
wc.Addr = connRt.Addr
|
||||
wc.Zone = connRt.Zone
|
||||
wc.PingServer = connRt.PingURL
|
||||
wc.Port = connRt.Port
|
||||
wc.Tag = connRt.Tag
|
||||
|
||||
addr := getIP(c.RemoteAddr())
|
||||
wc.Printf("id: %v | addr: %v | zone: %v | ping: %v | tag: %v", wc.Id, addr, wc.Zone, wc.PingServer, wc.Tag)
|
||||
wc.StunTurnServer = ice.ToJson(s.cfg.Webrtc.IceServers, ice.Replacement{From: "server-ip", To: addr})
|
||||
|
||||
// Attach to Server instance with workerID, add defer
|
||||
s.workerClients[workerID] = wc
|
||||
defer s.cleanWorker(wc, workerID)
|
||||
|
||||
wc.Send(api.ServerIdPacket(workerID), nil)
|
||||
|
||||
s.workerRoutes(wc)
|
||||
wc.Listen()
|
||||
}
|
||||
|
||||
// WS handles all connections from user/frontend to coordinator
|
||||
func (s *Server) WS(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println("Coordinator: A user is connecting...")
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("Warn: Something wrong. Recovered in ", r)
|
||||
}
|
||||
}()
|
||||
|
||||
c, err := s.userWsUpgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Println("Coordinator: [!] WS upgrade:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate sessionID for browserClient
|
||||
var sessionID string
|
||||
for {
|
||||
sessionID = uuid.Must(uuid.NewV4()).String()
|
||||
// check duplicate
|
||||
if _, ok := s.browserClients[sessionID]; !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Create browserClient instance
|
||||
bc := NewBrowserClient(c, sessionID)
|
||||
bc.Println("Generated worker ID")
|
||||
|
||||
// Run browser listener first (to capture ping)
|
||||
go bc.Listen()
|
||||
|
||||
/* Create a session - mapping browserClient with workerClient */
|
||||
var wc *WorkerClient
|
||||
|
||||
// get roomID if it is embeded in request. Server will pair the frontend with the server running the room. It only happens when we are trying to access a running room over share link.
|
||||
// TODO: Update link to the wiki
|
||||
q := r.URL.Query()
|
||||
roomID := q.Get("room_id")
|
||||
// zone param is to pick worker in that zone only
|
||||
// if there is no zone param, we can pick
|
||||
userZone := q.Get("zone")
|
||||
workerId := q.Get("wid")
|
||||
|
||||
// worker selection flow:
|
||||
// by room -> by id -> by address -> by zone
|
||||
|
||||
bc.Printf("Get Room %s Zone %s From URL %v", roomID, userZone, r.URL)
|
||||
|
||||
if roomID != "" {
|
||||
bc.Printf("Detected roomID %v from URL", roomID)
|
||||
if workerID, ok := s.roomToWorker[roomID]; ok {
|
||||
wc = s.workerClients[workerID]
|
||||
if userZone != "" && wc.Zone != userZone {
|
||||
// if there is zone param, we need to ensure the worker in that zone
|
||||
// if not we consider the room is missing
|
||||
wc = nil
|
||||
} else {
|
||||
bc.Printf("Found running server with id=%v client=%v", workerID, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no existing server to connect to, we find the best possible worker for the frontend
|
||||
if wc == nil {
|
||||
// when we select one particular worker
|
||||
if workerId != "" {
|
||||
if xid_, err := xid.FromString(workerId); err == nil {
|
||||
if s.cfg.Coordinator.Debug {
|
||||
for _, w := range s.getAvailableWorkers() {
|
||||
if xid_ == w.Id {
|
||||
wc = w
|
||||
bc.Printf("[!] Worker found: %v", xid_)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, w := range s.getAvailableWorkers() {
|
||||
if bytes.Equal(xid_.Machine(), w.Id.Machine()) {
|
||||
wc = w
|
||||
bc.Printf("[!] Machine %v found: %v", xid_.Machine(), xid_)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wc == nil {
|
||||
// Get the best server for frontend to connect to
|
||||
wc, err = s.getBestWorkerClient(bc, userZone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign available worker to browserClient
|
||||
bc.WorkerID = wc.WorkerID
|
||||
|
||||
wc.ChangeUserQuantityBy(1)
|
||||
defer wc.ChangeUserQuantityBy(-1)
|
||||
|
||||
// Everything is cool
|
||||
// Attach to Server instance with sessionID
|
||||
s.browserClients[sessionID] = bc
|
||||
defer s.cleanBrowser(bc, sessionID)
|
||||
|
||||
// Routing browserClient message
|
||||
s.useragentRoutes(bc)
|
||||
|
||||
bc.Send(cws.WSPacket{
|
||||
ID: "init",
|
||||
Data: createInitPackage(wc.Id, wc.StunTurnServer, s.library.GetAll()),
|
||||
}, nil)
|
||||
|
||||
// If peerconnection is done (client.Done is signalled), we close peerconnection
|
||||
<-bc.Done
|
||||
|
||||
// Notify worker to clean session
|
||||
wc.Send(api.TerminateSessionPacket(sessionID), nil)
|
||||
}
|
||||
|
||||
func (s *Server) getBestWorkerClient(client *BrowserClient, zone string) (*WorkerClient, error) {
|
||||
workerClients := s.getAvailableWorkers()
|
||||
serverID, err := s.findBestServerFromBrowser(workerClients, client, zone)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return nil, err
|
||||
}
|
||||
return s.workerClients[serverID], nil
|
||||
}
|
||||
|
||||
// getAvailableWorkers returns the list of available worker
|
||||
func (s *Server) getAvailableWorkers() map[string]*WorkerClient {
|
||||
workerClients := map[string]*WorkerClient{}
|
||||
for k, w := range s.workerClients {
|
||||
if w.HasGameSlot() {
|
||||
workerClients[k] = w
|
||||
}
|
||||
}
|
||||
return workerClients
|
||||
}
|
||||
|
||||
// findBestServerFromBrowser returns the best server for a session
|
||||
// All workers addresses are sent to user and user will ping to get latency
|
||||
func (s *Server) findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient, zone string) (string, error) {
|
||||
// TODO: Find best Server by latency, currently return by ping
|
||||
if len(workerClients) == 0 {
|
||||
return "", errors.New("no server found")
|
||||
}
|
||||
|
||||
latencies := s.getLatencyMapFromBrowser(workerClients, client)
|
||||
client.Println("Latency map", latencies)
|
||||
|
||||
if len(latencies) == 0 {
|
||||
return "", errors.New("no server found")
|
||||
}
|
||||
|
||||
var bestWorker *WorkerClient
|
||||
var minLatency int64 = math.MaxInt64
|
||||
|
||||
// get the worker with lowest latency to user
|
||||
for wc, l := range latencies {
|
||||
if zone != "" && wc.Zone != zone {
|
||||
// skip worker not in the zone if zone param is given
|
||||
continue
|
||||
}
|
||||
|
||||
if l < minLatency {
|
||||
bestWorker = wc
|
||||
minLatency = l
|
||||
}
|
||||
}
|
||||
|
||||
return bestWorker.WorkerID, nil
|
||||
}
|
||||
|
||||
// getLatencyMapFromBrowser get all latencies from worker to user
|
||||
func (s *Server) getLatencyMapFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) map[*WorkerClient]int64 {
|
||||
var workersList []*WorkerClient
|
||||
var addressList []string
|
||||
uniqueAddresses := map[string]bool{}
|
||||
latencyMap := map[*WorkerClient]int64{}
|
||||
|
||||
// addressList is the list of worker addresses
|
||||
for _, workerClient := range workerClients {
|
||||
if _, ok := uniqueAddresses[workerClient.PingServer]; !ok {
|
||||
addressList = append(addressList, workerClient.PingServer)
|
||||
}
|
||||
uniqueAddresses[workerClient.PingServer] = true
|
||||
workersList = append(workersList, workerClient)
|
||||
}
|
||||
|
||||
// send this address to user and get back latency
|
||||
client.Println("Send sync", addressList, strings.Join(addressList, ","))
|
||||
data := client.SyncSend(cws.WSPacket{
|
||||
ID: "checkLatency",
|
||||
Data: strings.Join(addressList, ","),
|
||||
})
|
||||
|
||||
respLatency := map[string]int64{}
|
||||
err := json.Unmarshal([]byte(data.Data), &respLatency)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return latencyMap
|
||||
}
|
||||
|
||||
for _, workerClient := range workersList {
|
||||
if latency, ok := respLatency[workerClient.PingServer]; ok {
|
||||
latencyMap[workerClient] = latency
|
||||
}
|
||||
}
|
||||
return latencyMap
|
||||
}
|
||||
|
||||
// cleanBrowser is called when a browser is disconnected
|
||||
func (s *Server) cleanBrowser(bc *BrowserClient, sessionID string) {
|
||||
bc.Println("Disconnect from coordinator")
|
||||
delete(s.browserClients, sessionID)
|
||||
bc.Close()
|
||||
}
|
||||
|
||||
// cleanWorker is called when a worker is disconnected
|
||||
// connection from worker to coordinator is also closed
|
||||
func (s *Server) cleanWorker(wc *WorkerClient, workerID string) {
|
||||
wc.Println("Unregister worker from coordinator")
|
||||
// Remove workerID from workerClients
|
||||
delete(s.workerClients, workerID)
|
||||
// Clean all rooms connecting to that server
|
||||
for roomID, roomServer := range s.roomToWorker {
|
||||
if roomServer == workerID {
|
||||
wc.Printf("Remove room %s", roomID)
|
||||
delete(s.roomToWorker, roomID)
|
||||
}
|
||||
}
|
||||
|
||||
wc.Close()
|
||||
}
|
||||
|
||||
// createInitPackage returns xid + serverhost + game list in encoded wspacket format
|
||||
// This package will be sent to initialize
|
||||
func createInitPackage(id xid.ID, stunturn string, games []games.GameMetadata) string {
|
||||
var gameName []string
|
||||
for _, game := range games {
|
||||
gameName = append(gameName, game.Name)
|
||||
}
|
||||
initPackage := append([]string{id.String(), stunturn}, gameName...)
|
||||
encodedList, _ := json.Marshal(initPackage)
|
||||
return string(encodedList)
|
||||
}
|
||||
|
||||
func getIP(a net.Addr) (addr string) {
|
||||
if parts := strings.Split(a.String(), ":"); len(parts) == 2 {
|
||||
addr = parts[0]
|
||||
}
|
||||
if addr == "" {
|
||||
return "localhost"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/shared"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network/httpx"
|
||||
)
|
||||
|
||||
func NewHTTPServer(conf coordinator.Config, fnMux func(mux *http.ServeMux)) (*httpx.Server, error) {
|
||||
return httpx.NewServer(
|
||||
conf.Coordinator.Server.GetAddr(),
|
||||
func(*httpx.Server) http.Handler {
|
||||
h := http.NewServeMux()
|
||||
h.Handle("/", index(conf))
|
||||
h.Handle("/static/", static("./web"))
|
||||
fnMux(h)
|
||||
return h
|
||||
},
|
||||
httpx.WithServerConfig(conf.Coordinator.Server),
|
||||
)
|
||||
}
|
||||
|
||||
func index(conf coordinator.Config) http.Handler {
|
||||
tpl, err := template.ParseFiles("./web/index.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// return 404 on unknown
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
// render index page with some tpl values
|
||||
tplData := struct {
|
||||
Analytics coordinator.Analytics
|
||||
Recording shared.Recording
|
||||
}{conf.Coordinator.Analytics, conf.Recording}
|
||||
if err = tpl.Execute(w, tplData); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func static(dir string) http.Handler {
|
||||
return http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))
|
||||
}
|
||||
162
pkg/coordinator/hub.go
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/service"
|
||||
)
|
||||
|
||||
type Hub struct {
|
||||
service.Service
|
||||
|
||||
conf coordinator.Config
|
||||
launcher games.Launcher
|
||||
users com.NetMap[*User]
|
||||
workers com.NetMap[*Worker]
|
||||
log *logger.Logger
|
||||
|
||||
wConn, uConn *com.Connector
|
||||
}
|
||||
|
||||
func NewHub(conf coordinator.Config, lib games.GameLibrary, log *logger.Logger) *Hub {
|
||||
return &Hub{
|
||||
conf: conf,
|
||||
users: com.NewNetMap[*User](),
|
||||
workers: com.NewNetMap[*Worker](),
|
||||
launcher: games.NewGameLauncher(lib),
|
||||
log: log,
|
||||
wConn: com.NewConnector(
|
||||
com.WithOrigin(conf.Coordinator.Origin.WorkerWs),
|
||||
com.WithTag("w"),
|
||||
),
|
||||
uConn: com.NewConnector(
|
||||
com.WithOrigin(conf.Coordinator.Origin.UserWs),
|
||||
com.WithTag("u"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// handleUserConnection handles all connections from user/frontend.
|
||||
func (h *Hub) handleUserConnection(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Info().Str("c", "u").Str("d", "←").Msgf("Handshake %v", r.Host)
|
||||
conn, err := h.uConn.NewClientServer(w, r, h.log)
|
||||
if err != nil {
|
||||
h.log.Error().Err(err).Msg("couldn't init user connection")
|
||||
}
|
||||
usr := NewUserConnection(conn)
|
||||
defer func() {
|
||||
if usr != nil {
|
||||
usr.Disconnect()
|
||||
h.users.Remove(usr)
|
||||
}
|
||||
}()
|
||||
usr.HandleRequests(h, h.launcher, h.conf)
|
||||
|
||||
q := r.URL.Query()
|
||||
roomId := q.Get(api.RoomIdQueryParam)
|
||||
zone := q.Get(api.ZoneQueryParam)
|
||||
wid := q.Get(api.WorkerIdParam)
|
||||
|
||||
usr.Log.Info().Msg("Search available workers")
|
||||
var wkr *Worker
|
||||
if wkr = h.findWorkerByRoom(roomId, zone); wkr != nil {
|
||||
usr.Log.Info().Str("room", roomId).Msg("An existing worker has been found")
|
||||
} else if wkr = h.findWorkerById(wid, h.conf.Coordinator.Debug); wkr != nil {
|
||||
usr.Log.Info().Msgf("Worker with id: %v has been found", wid)
|
||||
} else if h.conf.Coordinator.Selector == "" || h.conf.Coordinator.Selector == coordinator.SelectAny {
|
||||
usr.Log.Debug().Msgf("Searching any free worker...")
|
||||
if wkr = h.find1stFreeWorker(zone); wkr != nil {
|
||||
usr.Log.Info().Msgf("Found next free worker")
|
||||
}
|
||||
} else if h.conf.Coordinator.Selector == coordinator.SelectByPing {
|
||||
usr.Log.Debug().Msgf("Searching fastest free worker...")
|
||||
if wkr = h.findFastestWorker(zone,
|
||||
func(servers []string) (map[string]int64, error) { return usr.CheckLatency(servers) }); wkr != nil {
|
||||
usr.Log.Info().Msg("The fastest worker has been found")
|
||||
}
|
||||
}
|
||||
|
||||
if wkr == nil {
|
||||
usr.Log.Warn().Msg("no free workers")
|
||||
return
|
||||
}
|
||||
|
||||
usr.SetWorker(wkr)
|
||||
h.users.Add(usr)
|
||||
usr.InitSession(wkr.Id().String(), h.conf.Webrtc.IceServers, h.launcher.GetAppNames())
|
||||
<-usr.Done()
|
||||
}
|
||||
|
||||
// handleWorkerConnection handles all connections from a new worker to coordinator.
|
||||
func (h *Hub) handleWorkerConnection(w http.ResponseWriter, r *http.Request) {
|
||||
h.log.Info().Str("c", "w").Str("d", "←").Msgf("Handshake %v", r.Host)
|
||||
|
||||
data := r.URL.Query().Get(api.DataQueryParam)
|
||||
handshake, err := GetConnectionRequest(data)
|
||||
if err != nil || handshake == nil {
|
||||
h.log.Error().Err(err).Msg("got a malformed request")
|
||||
return
|
||||
}
|
||||
|
||||
if handshake.PingURL == "" {
|
||||
h.log.Warn().Msg("Ping address is not set")
|
||||
}
|
||||
|
||||
if h.conf.Coordinator.Server.Https && !handshake.IsHTTPS {
|
||||
h.log.Warn().Msg("Unsecure connection. The worker may not work properly without HTTPS on its side!")
|
||||
}
|
||||
|
||||
conn, err := h.wConn.NewClientServer(w, r, h.log)
|
||||
if err != nil {
|
||||
h.log.Error().Err(err).Msg("couldn't init worker connection")
|
||||
return
|
||||
}
|
||||
|
||||
worker := &Worker{
|
||||
SocketClient: *conn,
|
||||
Addr: handshake.Addr,
|
||||
PingServer: handshake.PingURL,
|
||||
Port: handshake.Port,
|
||||
Tag: handshake.Tag,
|
||||
Zone: handshake.Zone,
|
||||
}
|
||||
// we duplicate uid from the handshake
|
||||
hid := network.Uid(handshake.Id)
|
||||
if !(handshake.Id == "" || !network.ValidUid(hid)) {
|
||||
conn.SetId(hid)
|
||||
worker.Log.Debug().Msgf("connection id has been changed to %s", hid)
|
||||
}
|
||||
defer func() {
|
||||
if worker != nil {
|
||||
worker.Disconnect()
|
||||
h.workers.Remove(worker)
|
||||
}
|
||||
}()
|
||||
|
||||
h.log.Info().Msgf("New worker / addr: %v, port: %v, zone: %v, ping addr: %v, tag: %v",
|
||||
worker.Addr, worker.Port, worker.Zone, worker.PingServer, worker.Tag)
|
||||
worker.HandleRequests(&h.users)
|
||||
h.workers.Add(worker)
|
||||
worker.Listen()
|
||||
}
|
||||
|
||||
func (h *Hub) GetServerList() (r []api.Server) {
|
||||
for _, w := range h.workers.List() {
|
||||
r = append(r, api.Server{
|
||||
Addr: w.Addr,
|
||||
Id: w.Id(),
|
||||
IsBusy: !w.HasSlot(),
|
||||
PingURL: w.PingServer,
|
||||
Port: w.Port,
|
||||
Tag: w.Tag,
|
||||
Zone: w.Zone,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
)
|
||||
|
||||
func (wc *WorkerClient) handleHeartbeat() cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func GetConnectionRequest(data string) (api.ConnectionRequest, error) {
|
||||
req := api.ConnectionRequest{}
|
||||
if data == "" {
|
||||
return req, nil
|
||||
}
|
||||
decodeString, err := base64.URLEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return req, err
|
||||
}
|
||||
err = json.Unmarshal(decodeString, &req)
|
||||
return req, err
|
||||
}
|
||||
|
||||
// handleRegisterRoom event from a worker, when worker created a new room.
|
||||
// RoomID is global so it is managed by coordinator.
|
||||
func (wc *WorkerClient) handleRegisterRoom(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Printf("Coordinator: Received registerRoom room %s from worker %s", resp.Data, wc.WorkerID)
|
||||
s.roomToWorker[resp.Data] = wc.WorkerID
|
||||
log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
|
||||
return api.RegisterRoomPacket(api.NoData)
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetRoom returns the server ID based on requested roomID.
|
||||
func (wc *WorkerClient) handleGetRoom(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Println("Coordinator: Received a get room request")
|
||||
log.Println("Result: ", s.roomToWorker[resp.Data])
|
||||
return api.GetRoomPacket(s.roomToWorker[resp.Data])
|
||||
}
|
||||
}
|
||||
|
||||
// handleCloseRoom event from a worker, when worker close a room.
|
||||
func (wc *WorkerClient) handleCloseRoom(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
log.Printf("Coordinator: Received closeRoom room %s from worker %s", resp.Data, wc.WorkerID)
|
||||
delete(s.roomToWorker, resp.Data)
|
||||
log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
|
||||
return api.CloseRoomPacket(api.NoData)
|
||||
}
|
||||
}
|
||||
|
||||
// handleIceCandidate passes an ICE candidate (WebRTC) to the browser.
|
||||
func (wc *WorkerClient) handleIceCandidate(s *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket {
|
||||
wc.Println("Received IceCandidate from worker -> relay to browser")
|
||||
bc, ok := s.browserClients[resp.SessionID]
|
||||
if ok {
|
||||
// Remove SessionID while sending back to browser
|
||||
resp.SessionID = ""
|
||||
bc.Send(resp, nil)
|
||||
} else {
|
||||
wc.Println("Error: unknown SessionID:", resp.SessionID)
|
||||
}
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
|
||||
// workerRoutes adds all worker request routes.
|
||||
func (s *Server) workerRoutes(wc *WorkerClient) {
|
||||
if wc == nil {
|
||||
return
|
||||
}
|
||||
wc.Receive(api.Heartbeat, wc.handleHeartbeat())
|
||||
wc.Receive(api.RegisterRoom, wc.handleRegisterRoom(s))
|
||||
wc.Receive(api.GetRoom, wc.handleGetRoom(s))
|
||||
wc.Receive(api.CloseRoom, wc.handleCloseRoom(s))
|
||||
wc.Receive(api.IceCandidate, wc.handleIceCandidate(s))
|
||||
}
|
||||
|
||||
// useragentRoutes adds all useragent (browser) request routes.
|
||||
func (s *Server) useragentRoutes(bc *BrowserClient) {
|
||||
if bc == nil {
|
||||
return
|
||||
}
|
||||
bc.Receive(api.Heartbeat, bc.handleHeartbeat())
|
||||
bc.Receive(api.InitWebrtc, bc.handleInitWebrtc(s))
|
||||
bc.Receive(api.Answer, bc.handleAnswer(s))
|
||||
bc.Receive(api.IceCandidate, bc.handleIceCandidate(s))
|
||||
bc.Receive(api.GameStart, bc.handleGameStart(s))
|
||||
bc.Receive(api.GameQuit, bc.handleGameQuit(s))
|
||||
bc.Receive(api.GameSave, bc.handleGameSave(s))
|
||||
bc.Receive(api.GameLoad, bc.handleGameLoad(s))
|
||||
bc.Receive(api.GamePlayerSelect, bc.handleGamePlayerSelect(s))
|
||||
bc.Receive(api.GameMultitap, bc.handleGameMultitap(s))
|
||||
bc.Receive(api.GameRecording, bc.handleGameRecording(s))
|
||||
bc.Receive(api.GetServerList, bc.handleGetServerList(s))
|
||||
}
|
||||
89
pkg/coordinator/user.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/com"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
com.SocketClient
|
||||
w *Worker // linked worker
|
||||
}
|
||||
|
||||
// NewUserConnection supposed to be a bidirectional one.
|
||||
func NewUserConnection(conn *com.SocketClient) *User { return &User{SocketClient: *conn} }
|
||||
|
||||
func (u *User) SetWorker(w *Worker) { u.w = w; u.w.Reserve() }
|
||||
|
||||
func (u *User) Disconnect() {
|
||||
u.SocketClient.Close()
|
||||
if u.w != nil {
|
||||
u.w.UnReserve()
|
||||
u.w.TerminateSession(u.Id())
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleRequests(info api.HasServerInfo, launcher games.Launcher, conf coordinator.Config) {
|
||||
u.ProcessMessages()
|
||||
u.OnPacket(func(x com.In) error {
|
||||
// !to use proper channels
|
||||
switch x.T {
|
||||
case api.WebrtcInit:
|
||||
if u.w != nil {
|
||||
u.HandleWebrtcInit()
|
||||
}
|
||||
case api.WebrtcAnswer:
|
||||
rq := api.Unwrap[api.WebrtcAnswerUserRequest](x.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleWebrtcAnswer(*rq)
|
||||
case api.WebrtcIce:
|
||||
rq := api.Unwrap[api.WebrtcUserIceCandidate](x.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleWebrtcIceCandidate(*rq)
|
||||
case api.StartGame:
|
||||
rq := api.Unwrap[api.GameStartUserRequest](x.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleStartGame(*rq, launcher, conf)
|
||||
case api.QuitGame:
|
||||
rq := api.Unwrap[api.GameQuitRequest](x.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleQuitGame(*rq)
|
||||
case api.SaveGame:
|
||||
return u.HandleSaveGame()
|
||||
case api.LoadGame:
|
||||
return u.HandleLoadGame()
|
||||
case api.ChangePlayer:
|
||||
rq := api.Unwrap[api.ChangePlayerUserRequest](x.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleChangePlayer(*rq)
|
||||
case api.ToggleMultitap:
|
||||
u.HandleToggleMultitap()
|
||||
case api.RecordGame:
|
||||
if !conf.Recording.Enabled {
|
||||
return api.ErrForbidden
|
||||
}
|
||||
rq := api.Unwrap[api.RecordGameRequest](x.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
u.HandleRecordGame(*rq)
|
||||
case api.GetWorkerList:
|
||||
u.handleGetWorkerList(conf.Coordinator.Debug, info)
|
||||
default:
|
||||
u.Log.Warn().Msgf("Unknown packet: %+v", x)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -1,308 +0,0 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/session"
|
||||
)
|
||||
|
||||
func (bc *BrowserClient) handleHeartbeat() cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) cws.WSPacket { return resp }
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleInitWebrtc(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
// initWebrtc now only sends signal to worker, asks it to createOffer
|
||||
bc.Printf("Received init_webrtc request -> relay to worker: %s", bc.WorkerID)
|
||||
// relay request to target worker
|
||||
// worker creates a PeerConnection, and createOffer
|
||||
// send SDP back to browser
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
sdp := wc.SyncSend(resp)
|
||||
bc.Println("Received SDP from worker -> sending back to browser")
|
||||
return sdp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleAnswer(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
// contains SDP of browser createAnswer
|
||||
// forward to worker
|
||||
bc.Println("Received browser answered SDP -> relay to worker")
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
wc.Send(resp, nil)
|
||||
// no need to response
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleIceCandidate(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
// contains ICE candidate of browser
|
||||
// forward to worker
|
||||
bc.Println("Received IceCandidate from browser -> relay to worker")
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
wc.Send(resp, nil)
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameStart(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received start request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
|
||||
// +injects game data into the original game request
|
||||
gameStartCall, err := newGameStartCall(resp.RoomID, resp.Data, o.library, o.cfg.Recording.Enabled)
|
||||
if err != nil {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
if packet, err := gameStartCall.To(); err != nil {
|
||||
return cws.EmptyPacket
|
||||
} else {
|
||||
resp.Data = packet
|
||||
}
|
||||
workerResp := wc.SyncSend(resp)
|
||||
// Response from worker contains initialized roomID. Set roomID to the session
|
||||
bc.RoomID = workerResp.RoomID
|
||||
bc.Println("Received room response from browser: ", workerResp.RoomID)
|
||||
|
||||
if o.cfg.Recording.Enabled && gameStartCall.Record {
|
||||
bc.Send(cws.WSPacket{
|
||||
ID: api.GameRecording,
|
||||
Data: "ok",
|
||||
RoomID: workerResp.RoomID,
|
||||
PlayerIndex: workerResp.PlayerIndex,
|
||||
PacketID: workerResp.PacketID,
|
||||
SessionID: workerResp.SessionID,
|
||||
}, func(response cws.WSPacket) {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
return workerResp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameQuit(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received quit request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
// Send but, waiting
|
||||
wc.SyncSend(resp)
|
||||
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameSave(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received save request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameLoad(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received load request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGamePlayerSelect(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received update player index request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameMultitap(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received multitap request from a browser -> relay to worker")
|
||||
|
||||
// TODO: Async
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGameRecording(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
bc.Println("Received recording request from a browser -> relay to worker")
|
||||
|
||||
if !o.cfg.Recording.Enabled {
|
||||
bc.Printf("Recording should be disabled!")
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
|
||||
request := api.GameRecordingRequest{}
|
||||
if err := request.From(resp.Data); err != nil {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
|
||||
bc.Printf("Session: %v, room: %v, rec: %v user: %v", bc.SessionID, bc.RoomID, request.Active, request.User)
|
||||
|
||||
if bc.RoomID == "" {
|
||||
bc.Printf("Recording in the empty room is not allowed!")
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
|
||||
resp.SessionID = bc.SessionID
|
||||
resp.RoomID = bc.RoomID
|
||||
wc, ok := o.workerClients[bc.WorkerID]
|
||||
if !ok {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
resp = wc.SyncSend(resp)
|
||||
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
// newGameStartCall gathers data for a new game start call of the worker
|
||||
func newGameStartCall(roomId string, data string, library games.GameLibrary, recording bool) (api.GameStartCall, error) {
|
||||
request := api.GameStartRequest{}
|
||||
if err := request.From(data); err != nil {
|
||||
return api.GameStartCall{}, errors.New("invalid request")
|
||||
}
|
||||
|
||||
// the name of the game either in the `room id` field or
|
||||
// it's in the initial request
|
||||
game := request.GameName
|
||||
if roomId != "" {
|
||||
// ! should be moved into coordinator
|
||||
name := session.GetGameNameFromRoomID(roomId)
|
||||
if name == "" {
|
||||
return api.GameStartCall{}, errors.New("couldn't decode game name from the room id")
|
||||
}
|
||||
game = name
|
||||
}
|
||||
|
||||
gameInfo := library.FindGameByName(game)
|
||||
if gameInfo.Path == "" {
|
||||
return api.GameStartCall{}, fmt.Errorf("couldn't find game info for the game %v", game)
|
||||
}
|
||||
|
||||
call := api.GameStartCall{
|
||||
Name: gameInfo.Name,
|
||||
Base: gameInfo.Base,
|
||||
Path: gameInfo.Path,
|
||||
Type: gameInfo.Type,
|
||||
}
|
||||
if recording {
|
||||
call.Record = request.Record
|
||||
call.RecordUser = request.RecordUser
|
||||
}
|
||||
return call, nil
|
||||
}
|
||||
|
||||
func (bc *BrowserClient) handleGetServerList(o *Server) cws.PacketHandler {
|
||||
return func(resp cws.WSPacket) (req cws.WSPacket) {
|
||||
var request api.GetServerListRequest
|
||||
if err := request.From(resp.Data); err != nil {
|
||||
return cws.EmptyPacket
|
||||
}
|
||||
response := api.GetServerListResponse{}
|
||||
var servers []api.Server
|
||||
if o.cfg.Coordinator.Debug {
|
||||
for _, s := range o.workerClients {
|
||||
servers = append(servers, api.Server{
|
||||
Addr: s.Addr, Id: s.WorkerID, IsBusy: !s.HasGameSlot(), PingURL: s.PingServer, Port: s.Port,
|
||||
Tag: s.Tag, Zone: s.Zone, Xid: s.Id.String(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// not sure if []byte to string always reversible :/
|
||||
unique := map[string]*api.Server{}
|
||||
for _, s := range o.workerClients {
|
||||
mid := string(s.Id.Machine())
|
||||
if _, ok := unique[mid]; !ok {
|
||||
unique[mid] = &api.Server{Addr: s.Addr, PingURL: s.PingServer, Xid: s.Id.String()}
|
||||
}
|
||||
unique[mid].Replicas++
|
||||
}
|
||||
for _, v := range unique {
|
||||
servers = append(servers, *v)
|
||||
}
|
||||
}
|
||||
sort.SliceStable(servers, func(i, j int) bool {
|
||||
if servers[i].Addr != servers[j].Addr {
|
||||
return servers[i].Addr < servers[j].Addr
|
||||
}
|
||||
return servers[i].Port < servers[j].Port
|
||||
})
|
||||
response.Servers = servers
|
||||
if packet, err := response.To(); err != nil {
|
||||
return cws.EmptyPacket
|
||||
} else {
|
||||
resp.Data = packet
|
||||
}
|
||||
return resp
|
||||
}
|
||||
}
|
||||
37
pkg/coordinator/userapi.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
|
||||
)
|
||||
|
||||
// CheckLatency sends a list of server addresses to the user
|
||||
// and waits get back this list with tested ping times for each server.
|
||||
func (u *User) CheckLatency(req api.CheckLatencyUserResponse) (api.CheckLatencyUserRequest, error) {
|
||||
data, err := u.Send(api.CheckLatency, req)
|
||||
if err != nil || data == nil {
|
||||
return nil, err
|
||||
}
|
||||
dat := api.Unwrap[api.CheckLatencyUserRequest](data)
|
||||
if dat == nil {
|
||||
return api.CheckLatencyUserRequest{}, err
|
||||
}
|
||||
return *dat, err
|
||||
}
|
||||
|
||||
// InitSession signals the user that the app is ready to go.
|
||||
func (u *User) InitSession(wid string, ice []webrtc.IceServer, games []string) {
|
||||
// don't do this at home
|
||||
u.Notify(api.InitSessionResult(*(*[]api.IceServer)(unsafe.Pointer(&ice)), games, wid))
|
||||
}
|
||||
|
||||
// SendWebrtcOffer sends SDP offer back to the user.
|
||||
func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
|
||||
|
||||
// SendWebrtcIceCandidate sends remote ICE candidate back to the user.
|
||||
func (u *User) SendWebrtcIceCandidate(candidate string) { u.Notify(api.WebrtcIce, candidate) }
|
||||
|
||||
// StartGame signals the user that everything is ready to start a game.
|
||||
func (u *User) StartGame() { u.Notify(api.StartGame, u.w.RoomId) }
|
||||
151
pkg/coordinator/userhandlers.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
)
|
||||
|
||||
func (u *User) HandleWebrtcInit() {
|
||||
resp, err := u.w.WebrtcInit(u.Id())
|
||||
if err != nil || resp == nil || *resp == api.EMPTY {
|
||||
u.Log.Error().Err(err).Msg("malformed WebRTC init response")
|
||||
return
|
||||
}
|
||||
u.SendWebrtcOffer(string(*resp))
|
||||
}
|
||||
|
||||
func (u *User) HandleWebrtcAnswer(rq api.WebrtcAnswerUserRequest) {
|
||||
u.w.WebrtcAnswer(u.Id(), string(rq))
|
||||
}
|
||||
|
||||
func (u *User) HandleWebrtcIceCandidate(rq api.WebrtcUserIceCandidate) {
|
||||
u.w.WebrtcIceCandidate(u.Id(), string(rq))
|
||||
}
|
||||
|
||||
func (u *User) HandleStartGame(rq api.GameStartUserRequest, launcher games.Launcher, conf coordinator.Config) {
|
||||
// +injects game data into the original game request
|
||||
// the name of the game either in the `room id` field or
|
||||
// it's in the initial request
|
||||
game := rq.GameName
|
||||
if rq.RoomId != "" {
|
||||
name := launcher.ExtractAppNameFromUrl(rq.RoomId)
|
||||
if name == "" {
|
||||
u.Log.Warn().Msg("couldn't decode game name from the room id")
|
||||
return
|
||||
}
|
||||
game = name
|
||||
}
|
||||
|
||||
gameInfo, err := launcher.FindAppByName(game)
|
||||
if err != nil {
|
||||
u.Log.Error().Err(err).Str("game", game).Msg("couldn't find game info")
|
||||
return
|
||||
}
|
||||
|
||||
startGameResp, err := u.w.StartGame(u.Id(), gameInfo, rq)
|
||||
if err != nil || startGameResp == nil {
|
||||
u.Log.Error().Err(err).Msg("malformed game start response")
|
||||
return
|
||||
}
|
||||
if startGameResp.Rid == "" {
|
||||
u.Log.Error().Msg("there is no room")
|
||||
return
|
||||
}
|
||||
u.Log.Info().Str("id", startGameResp.Rid).Msg("Received room response from worker")
|
||||
u.StartGame()
|
||||
|
||||
// send back recording status
|
||||
if conf.Recording.Enabled && rq.Record {
|
||||
u.Notify(api.RecordGame, api.OK)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleQuitGame(rq api.GameQuitRequest) {
|
||||
if rq.Room.Rid == u.w.RoomId {
|
||||
u.w.QuitGame(u.Id())
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) HandleSaveGame() error {
|
||||
resp, err := u.w.SaveGame(u.Id())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Notify(api.SaveGame, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HandleLoadGame() error {
|
||||
resp, err := u.w.LoadGame(u.Id())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Notify(api.LoadGame, resp)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HandleChangePlayer(rq api.ChangePlayerUserRequest) {
|
||||
resp, err := u.w.ChangePlayer(u.Id(), int(rq))
|
||||
// !to make it a little less convoluted
|
||||
if err != nil || resp == nil || *resp == -1 {
|
||||
u.Log.Error().Err(err).Msg("player switch failed for some reason")
|
||||
return
|
||||
}
|
||||
u.Notify(api.ChangePlayer, rq)
|
||||
}
|
||||
|
||||
func (u *User) HandleToggleMultitap() { u.w.ToggleMultitap(u.Id()) }
|
||||
|
||||
func (u *User) HandleRecordGame(rq api.RecordGameRequest) {
|
||||
if u.w == nil {
|
||||
return
|
||||
}
|
||||
|
||||
u.Log.Debug().Msgf("??? room: %v, rec: %v user: %v", u.w.RoomId, rq.Active, rq.User)
|
||||
|
||||
if u.w.RoomId == "" {
|
||||
u.Log.Error().Msg("Recording in the empty room is not allowed!")
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := u.w.RecordGame(u.Id(), rq.Active, rq.User)
|
||||
if err != nil {
|
||||
u.Log.Error().Err(err).Msg("malformed game record request")
|
||||
return
|
||||
}
|
||||
u.Notify(api.RecordGame, resp)
|
||||
}
|
||||
|
||||
func (u *User) handleGetWorkerList(debug bool, info api.HasServerInfo) {
|
||||
response := api.GetWorkerListResponse{}
|
||||
servers := info.GetServerList()
|
||||
|
||||
if debug {
|
||||
response.Servers = servers
|
||||
} else {
|
||||
// not sure if []byte to string always reversible :/
|
||||
unique := map[string]*api.Server{}
|
||||
for _, s := range servers {
|
||||
mid := s.Id.Machine()
|
||||
if _, ok := unique[mid]; !ok {
|
||||
unique[mid] = &api.Server{Addr: s.Addr, PingURL: s.PingURL, Id: s.Id, InGroup: true}
|
||||
}
|
||||
unique[mid].Replicas++
|
||||
}
|
||||
for _, v := range unique {
|
||||
response.Servers = append(response.Servers, *v)
|
||||
}
|
||||
}
|
||||
if len(response.Servers) > 0 {
|
||||
sort.SliceStable(response.Servers, func(i, j int) bool {
|
||||
if response.Servers[i].Addr != response.Servers[j].Addr {
|
||||
return response.Servers[i].Addr < response.Servers[j].Addr
|
||||
}
|
||||
return response.Servers[i].Port < response.Servers[j].Port
|
||||
})
|
||||
}
|
||||
u.Notify(api.GetWorkerList, response)
|
||||
}
|
||||
|
|
@ -1,68 +1,81 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rs/xid"
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/com"
|
||||
)
|
||||
|
||||
type WorkerClient struct {
|
||||
*cws.Client
|
||||
type Worker struct {
|
||||
com.SocketClient
|
||||
com.RegionalClient
|
||||
slotted
|
||||
|
||||
Id xid.ID
|
||||
|
||||
WorkerID string
|
||||
Addr string
|
||||
// public server used for ping check
|
||||
PingServer string
|
||||
Port string
|
||||
StunTurnServer string
|
||||
Tag string
|
||||
userCount int // may be atomic
|
||||
Zone string
|
||||
|
||||
mu sync.Mutex
|
||||
Addr string
|
||||
PingServer string
|
||||
Port string
|
||||
RoomId string // room reference
|
||||
Tag string
|
||||
Zone string
|
||||
}
|
||||
|
||||
// NewWorkerClient returns a client connecting to worker.
|
||||
// This connection exchanges information between workers and server.
|
||||
func NewWorkerClient(c *websocket.Conn, workerID string) *WorkerClient {
|
||||
return &WorkerClient{
|
||||
Client: cws.NewClient(c),
|
||||
WorkerID: workerID,
|
||||
func (w *Worker) HandleRequests(users *com.NetMap[*User]) {
|
||||
// !to make a proper multithreading abstraction
|
||||
w.OnPacket(func(p com.In) error {
|
||||
switch p.T {
|
||||
case api.RegisterRoom:
|
||||
rq := api.Unwrap[api.RegisterRoomRequest](p.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
w.Log.Info().Msgf("set room [%v] = %v", w.Id(), *rq)
|
||||
w.HandleRegisterRoom(*rq)
|
||||
case api.CloseRoom:
|
||||
rq := api.Unwrap[api.CloseRoomRequest](p.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
w.HandleCloseRoom(*rq)
|
||||
case api.IceCandidate:
|
||||
rq := api.Unwrap[api.WebrtcIceCandidateRequest](p.Payload)
|
||||
if rq == nil {
|
||||
return api.ErrMalformed
|
||||
}
|
||||
w.HandleIceCandidate(*rq, users)
|
||||
default:
|
||||
w.Log.Warn().Msgf("Unknown packet: %+v", p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// In say whether some worker from this region (zone).
|
||||
// Empty region always returns true.
|
||||
func (w *Worker) In(region string) bool { return region == "" || region == w.Zone }
|
||||
|
||||
// slotted used for tracking user slots and the availability.
|
||||
type slotted int32
|
||||
|
||||
// HasSlot checks if the current worker has a free slot to start a new game.
|
||||
// Workers support only one game at a time, so it returns true in case if
|
||||
// there are no players in the room (worker).
|
||||
func (s *slotted) HasSlot() bool { return atomic.LoadInt32((*int32)(s)) == 0 }
|
||||
|
||||
// Reserve increments user counter of the worker.
|
||||
func (s *slotted) Reserve() { atomic.AddInt32((*int32)(s), 1) }
|
||||
|
||||
// UnReserve decrements user counter of the worker.
|
||||
func (s *slotted) UnReserve() {
|
||||
if atomic.AddInt32((*int32)(s), -1) < 0 {
|
||||
atomic.StoreInt32((*int32)(s), 0)
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeUserQuantityBy increases or decreases the total amount of
|
||||
// users connected to the current worker.
|
||||
// We count users to determine when the worker becomes new game ready.
|
||||
func (wc *WorkerClient) ChangeUserQuantityBy(n int) {
|
||||
wc.mu.Lock()
|
||||
wc.userCount += n
|
||||
// just to be on a safe side
|
||||
if wc.userCount < 0 {
|
||||
wc.userCount = 0
|
||||
}
|
||||
wc.mu.Unlock()
|
||||
}
|
||||
func (s *slotted) FreeSlots() { atomic.StoreInt32((*int32)(s), 0) }
|
||||
|
||||
// HasGameSlot tells whether the current worker has a
|
||||
// free slot to start a new game.
|
||||
// Workers support only one game at a time.
|
||||
func (wc *WorkerClient) HasGameSlot() bool {
|
||||
wc.mu.Lock()
|
||||
defer wc.mu.Unlock()
|
||||
return wc.userCount == 0
|
||||
}
|
||||
|
||||
func (wc *WorkerClient) Printf(format string, args ...interface{}) {
|
||||
log.Printf(fmt.Sprintf("Worker %s] %s", wc.WorkerID, format), args...)
|
||||
}
|
||||
|
||||
func (wc *WorkerClient) Println(args ...interface{}) {
|
||||
log.Println(fmt.Sprintf("Worker %s] %s", wc.WorkerID, fmt.Sprint(args...)))
|
||||
func (w *Worker) Disconnect() {
|
||||
w.SocketClient.Close()
|
||||
w.RoomId = ""
|
||||
w.FreeSlots()
|
||||
}
|
||||
|
|
|
|||
62
pkg/coordinator/workerapi.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/games"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/network"
|
||||
)
|
||||
|
||||
func (w *Worker) WebrtcInit(id network.Uid) (*api.WebrtcInitResponse, error) {
|
||||
return api.UnwrapChecked[api.WebrtcInitResponse](
|
||||
w.Send(api.WebrtcInit, api.WebrtcInitRequest{Stateful: api.Stateful{Id: id}}))
|
||||
}
|
||||
|
||||
func (w *Worker) WebrtcAnswer(id network.Uid, sdp string) {
|
||||
w.Notify(api.WebrtcAnswer, api.WebrtcAnswerRequest{Stateful: api.Stateful{Id: id}, Sdp: sdp})
|
||||
}
|
||||
|
||||
func (w *Worker) WebrtcIceCandidate(id network.Uid, can string) {
|
||||
w.Notify(api.NewWebrtcIceCandidateRequest(id, can))
|
||||
}
|
||||
|
||||
func (w *Worker) StartGame(id network.Uid, app games.AppMeta, req api.GameStartUserRequest) (*api.StartGameResponse, error) {
|
||||
return api.UnwrapChecked[api.StartGameResponse](w.Send(api.StartGame, api.StartGameRequest{
|
||||
StatefulRoom: api.StateRoom(id, req.RoomId),
|
||||
Game: api.GameInfo{Name: app.Name, Base: app.Base, Path: app.Path, Type: app.Type},
|
||||
PlayerIndex: req.PlayerIndex,
|
||||
Record: req.Record,
|
||||
RecordUser: req.RecordUser,
|
||||
}))
|
||||
}
|
||||
|
||||
func (w *Worker) QuitGame(id network.Uid) {
|
||||
w.Notify(api.QuitGame, api.GameQuitRequest{StatefulRoom: api.StateRoom(id, w.RoomId)})
|
||||
}
|
||||
|
||||
func (w *Worker) SaveGame(id network.Uid) (*api.SaveGameResponse, error) {
|
||||
return api.UnwrapChecked[api.SaveGameResponse](
|
||||
w.Send(api.SaveGame, api.SaveGameRequest{StatefulRoom: api.StateRoom(id, w.RoomId)}))
|
||||
}
|
||||
|
||||
func (w *Worker) LoadGame(id network.Uid) (*api.LoadGameResponse, error) {
|
||||
return api.UnwrapChecked[api.LoadGameResponse](
|
||||
w.Send(api.LoadGame, api.LoadGameRequest{StatefulRoom: api.StateRoom(id, w.RoomId)}))
|
||||
}
|
||||
|
||||
func (w *Worker) ChangePlayer(id network.Uid, index int) (*api.ChangePlayerResponse, error) {
|
||||
return api.UnwrapChecked[api.ChangePlayerResponse](
|
||||
w.Send(api.ChangePlayer, api.ChangePlayerRequest{StatefulRoom: api.StateRoom(id, w.RoomId), Index: index}))
|
||||
}
|
||||
|
||||
func (w *Worker) ToggleMultitap(id network.Uid) {
|
||||
_, _ = w.Send(api.ToggleMultitap, api.ToggleMultitapRequest{StatefulRoom: api.StateRoom(id, w.RoomId)})
|
||||
}
|
||||
|
||||
func (w *Worker) RecordGame(id network.Uid, rec bool, recUser string) (*api.RecordGameResponse, error) {
|
||||
return api.UnwrapChecked[api.RecordGameResponse](
|
||||
w.Send(api.RecordGame, api.RecordGameRequest{StatefulRoom: api.StateRoom(id, w.RoomId), Active: rec, User: recUser}))
|
||||
}
|
||||
|
||||
func (w *Worker) TerminateSession(id network.Uid) {
|
||||
_, _ = w.Send(api.TerminateSession, api.TerminateSessionRequest{Stateful: api.Stateful{Id: id}})
|
||||
}
|
||||
31
pkg/coordinator/workerhandlers.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package coordinator
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/api"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/com"
|
||||
)
|
||||
|
||||
func GetConnectionRequest(data string) (*api.ConnectionRequest, error) {
|
||||
if data == "" {
|
||||
return nil, fmt.Errorf("no data")
|
||||
}
|
||||
return api.UnwrapChecked[api.ConnectionRequest](base64.URLEncoding.DecodeString(data))
|
||||
}
|
||||
|
||||
func (w *Worker) HandleRegisterRoom(rq api.RegisterRoomRequest) { w.RoomId = string(rq) }
|
||||
func (w *Worker) HandleCloseRoom(rq api.CloseRoomRequest) {
|
||||
if string(rq) == w.RoomId {
|
||||
w.RoomId = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) HandleIceCandidate(rq api.WebrtcIceCandidateRequest, users *com.NetMap[*User]) {
|
||||
if usr, err := users.Find(string(rq.Id)); err == nil {
|
||||
usr.SendWebrtcIceCandidate(rq.Candidate)
|
||||
} else {
|
||||
w.Log.Warn().Str("id", rq.Id.String()).Msg("unknown session")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package api
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// This list of postfixes is used in the API:
|
||||
// - *Request postfix denotes clients calls (i.e. from a browser to the HTTP-server).
|
||||
// - *Call postfix denotes IPC calls (from the coordinator to a worker).
|
||||
|
||||
func from(source interface{}, data string) error {
|
||||
err := json.Unmarshal([]byte(data), source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func to(target interface{}) (string, error) {
|
||||
b, err := json.Marshal(target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
package api
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
|
||||
const (
|
||||
GetRoom = "get_room"
|
||||
CloseRoom = "close_room"
|
||||
RegisterRoom = "register_room"
|
||||
Heartbeat = "heartbeat"
|
||||
IceCandidate = "ice_candidate"
|
||||
|
||||
NoData = ""
|
||||
|
||||
InitWebrtc = "init_webrtc"
|
||||
Answer = "answer"
|
||||
|
||||
GameStart = "start"
|
||||
GameQuit = "quit"
|
||||
GameSave = "save"
|
||||
GameLoad = "load"
|
||||
GamePlayerSelect = "player_index"
|
||||
GameMultitap = "multitap"
|
||||
GameRecording = "recording"
|
||||
GetServerList = "get_server_list"
|
||||
)
|
||||
|
||||
type GameStartRequest struct {
|
||||
GameName string `json:"game_name"`
|
||||
Record bool `json:"record,omitempty"`
|
||||
RecordUser string `json:"record_user,omitempty"`
|
||||
}
|
||||
|
||||
func (packet *GameStartRequest) From(data string) error { return from(packet, data) }
|
||||
|
||||
type GameRecordingRequest struct {
|
||||
Active bool `json:"active"`
|
||||
User string `json:"user"`
|
||||
}
|
||||
|
||||
func (packet *GameRecordingRequest) From(data string) error { return from(packet, data) }
|
||||
|
||||
type GameStartCall struct {
|
||||
Name string `json:"name"`
|
||||
Base string `json:"base"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Record bool `json:"record,omitempty"`
|
||||
RecordUser string `json:"record_user,omitempty"`
|
||||
}
|
||||
|
||||
func (packet *GameStartCall) From(data string) error { return from(packet, data) }
|
||||
func (packet *GameStartCall) To() (string, error) { return to(packet) }
|
||||
|
||||
type ConnectionRequest struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
IsHTTPS bool `json:"is_https,omitempty"`
|
||||
PingURL string `json:"ping_url,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
Xid string `json:"xid,omitempty"`
|
||||
}
|
||||
|
||||
type GetServerListRequest struct{}
|
||||
type GetServerListResponse struct {
|
||||
Servers []Server `json:"servers"`
|
||||
}
|
||||
|
||||
// Server contains a list of server groups.
|
||||
// Server is a separate machine that may contain
|
||||
// multiple sub-processes.
|
||||
type Server struct {
|
||||
Addr string `json:"addr,omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
IsBusy bool `json:"is_busy,omitempty"`
|
||||
PingURL string `json:"ping_url"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Replicas uint32 `json:"replicas,omitempty"`
|
||||
Tag string `json:"tag,omitempty"`
|
||||
Zone string `json:"zone,omitempty"`
|
||||
Xid string `json:"xid,omitempty"`
|
||||
}
|
||||
|
||||
func (packet *GetServerListRequest) From(data string) error { return from(packet, data) }
|
||||
func (packet *GetServerListResponse) To() (string, error) { return to(packet) }
|
||||
|
||||
// packets
|
||||
|
||||
func RegisterRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: RegisterRoom, Data: data} }
|
||||
func GetRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: GetRoom, Data: data} }
|
||||
func CloseRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: CloseRoom, Data: data} }
|
||||
func IceCandidatePacket(data string, sessionId string) cws.WSPacket {
|
||||
return cws.WSPacket{ID: IceCandidate, Data: data, SessionID: sessionId}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package api
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/cws"
|
||||
|
||||
const (
|
||||
ServerId = "server_id"
|
||||
TerminateSession = "terminateSession"
|
||||
)
|
||||
|
||||
type ConfPushCall struct {
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
|
||||
func (packet *ConfPushCall) From(data string) error { return from(packet, data) }
|
||||
func (packet *ConfPushCall) To() (string, error) { return to(packet) }
|
||||
|
||||
func ServerIdPacket(id string) cws.WSPacket { return cws.WSPacket{ID: ServerId, Data: id} }
|
||||
func ConfigRequestPacket(conf []byte) cws.WSPacket { return cws.WSPacket{Data: string(conf)} }
|
||||
func TerminateSessionPacket(sessionId string) cws.WSPacket {
|
||||
return cws.WSPacket{ID: TerminateSession, SessionID: sessionId}
|
||||
}
|
||||
219
pkg/cws/cws.go
|
|
@ -1,219 +0,0 @@
|
|||
package cws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type (
|
||||
Client struct {
|
||||
id string
|
||||
|
||||
conn *websocket.Conn
|
||||
|
||||
sendLock sync.Mutex
|
||||
// sendCallback is callback based on packetID
|
||||
sendCallback map[string]func(req WSPacket)
|
||||
sendCallbackLock sync.Mutex
|
||||
// recvCallback is callback when receive based on ID of the packet
|
||||
recvCallback map[string]func(req WSPacket)
|
||||
|
||||
Done chan struct{}
|
||||
}
|
||||
|
||||
WSPacket struct {
|
||||
ID string `json:"id"`
|
||||
// TODO: Make Data generic: map[string]interface{} for more usecases
|
||||
Data string `json:"data"`
|
||||
|
||||
RoomID string `json:"room_id"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
|
||||
PacketID string `json:"packet_id"`
|
||||
// Globally ID of a browser session
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
PacketHandler func(resp WSPacket) (req WSPacket)
|
||||
)
|
||||
|
||||
var (
|
||||
EmptyPacket = WSPacket{}
|
||||
HeartbeatPacket = WSPacket{ID: "heartbeat"}
|
||||
)
|
||||
|
||||
const WSWait = 20 * time.Second
|
||||
|
||||
func NewClient(conn *websocket.Conn) *Client {
|
||||
id := uuid.Must(uuid.NewV4()).String()
|
||||
sendCallback := map[string]func(WSPacket){}
|
||||
recvCallback := map[string]func(WSPacket){}
|
||||
|
||||
return &Client{
|
||||
id: id,
|
||||
conn: conn,
|
||||
|
||||
sendCallback: sendCallback,
|
||||
recvCallback: recvCallback,
|
||||
|
||||
Done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Send sends a packet and trigger callback when the packet comes back
|
||||
func (c *Client) Send(request WSPacket, callback func(response WSPacket)) {
|
||||
request.PacketID = uuid.Must(uuid.NewV4()).String()
|
||||
data, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Consider using lock free
|
||||
// Wrap callback with sessionID and packetID
|
||||
if callback != nil {
|
||||
wrapperCallback := func(resp WSPacket) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("Recovered from err in client callback ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
resp.PacketID = request.PacketID
|
||||
resp.SessionID = request.SessionID
|
||||
callback(resp)
|
||||
}
|
||||
c.sendCallbackLock.Lock()
|
||||
c.sendCallback[request.PacketID] = wrapperCallback
|
||||
c.sendCallbackLock.Unlock()
|
||||
}
|
||||
|
||||
c.sendLock.Lock()
|
||||
c.conn.SetWriteDeadline(time.Now().Add(WSWait))
|
||||
c.conn.WriteMessage(websocket.TextMessage, data)
|
||||
c.sendLock.Unlock()
|
||||
}
|
||||
|
||||
// Receive receive and response back
|
||||
func (c *Client) Receive(id string, f PacketHandler) {
|
||||
c.recvCallback[id] = func(response WSPacket) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Println("Recovered from err ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
req := f(response)
|
||||
// Add Meta data
|
||||
req.PacketID = response.PacketID
|
||||
req.SessionID = response.SessionID
|
||||
|
||||
// Skip response if it is EmptyPacket
|
||||
if response == EmptyPacket {
|
||||
return
|
||||
}
|
||||
resp, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Println("[!] json marshal error:", err)
|
||||
}
|
||||
c.sendLock.Lock()
|
||||
c.conn.SetWriteDeadline(time.Now().Add(WSWait))
|
||||
c.conn.WriteMessage(websocket.TextMessage, resp)
|
||||
c.sendLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SyncSend sends a packet and wait for callback till the packet comes back
|
||||
func (c *Client) SyncSend(request WSPacket) (response WSPacket) {
|
||||
res := make(chan WSPacket)
|
||||
f := func(resp WSPacket) {
|
||||
res <- resp
|
||||
}
|
||||
c.Send(request, f)
|
||||
return <-res
|
||||
}
|
||||
|
||||
// SendAwait sends some packet while waiting for a tile-limited response
|
||||
//func (c *Client) SendAwait(packet WSPacket) WSPacket {
|
||||
// ch := make(chan WSPacket)
|
||||
// defer close(ch)
|
||||
// c.Send(packet, func(response WSPacket) { ch <- response })
|
||||
//
|
||||
// for {
|
||||
// select {
|
||||
// case packet := <-ch:
|
||||
// return packet
|
||||
// case <-time.After(config.WsIpcTimeout):
|
||||
// log.Printf("Packet receive timeout!")
|
||||
// return EmptyPacket
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
// Heartbeat maintains connection to coordinator.
|
||||
// Blocking.
|
||||
func (c *Client) Heartbeat() {
|
||||
// send heartbeat every 1s
|
||||
t := time.NewTicker(time.Second)
|
||||
// don't wait 1 second
|
||||
c.Send(HeartbeatPacket, nil)
|
||||
for {
|
||||
select {
|
||||
case <-c.Done:
|
||||
t.Stop()
|
||||
log.Printf("Close heartbeat")
|
||||
return
|
||||
case <-t.C:
|
||||
c.Send(HeartbeatPacket, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Listen() {
|
||||
for {
|
||||
c.conn.SetReadDeadline(time.Now().Add(WSWait))
|
||||
_, rawMsg, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("[!] read:", err)
|
||||
// TODO: Check explicit disconnect error to break
|
||||
close(c.Done)
|
||||
break
|
||||
}
|
||||
wspacket := WSPacket{}
|
||||
err = json.Unmarshal(rawMsg, &wspacket)
|
||||
|
||||
if err != nil {
|
||||
log.Println("Warn: error decoding", rawMsg)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if some async send is waiting for the response based on packetID
|
||||
// TODO: Change to read lock.
|
||||
//c.sendCallbackLock.Lock()
|
||||
callback, ok := c.sendCallback[wspacket.PacketID]
|
||||
//c.sendCallbackLock.Unlock()
|
||||
if ok {
|
||||
go callback(wspacket)
|
||||
//c.sendCallbackLock.Lock()
|
||||
delete(c.sendCallback, wspacket.PacketID)
|
||||
//c.sendCallbackLock.Unlock()
|
||||
// Skip receiveCallback to avoid duplication
|
||||
continue
|
||||
}
|
||||
// Check if some receiver with the ID is registered
|
||||
if callback, ok := c.recvCallback[wspacket.ID]; ok {
|
||||
go callback(wspacket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) Close() {
|
||||
if c == nil || c.conn == nil {
|
||||
return
|
||||
}
|
||||
c.conn.Close()
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package backend
|
||||
|
||||
type Download struct {
|
||||
Key string
|
||||
Address string
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
Request(dest string, urls ...Download) ([]string, []string)
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package backend
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/cavaliercoder/grab"
|
||||
)
|
||||
|
||||
type GrabDownloader struct {
|
||||
client *grab.Client
|
||||
concurrency int
|
||||
}
|
||||
|
||||
func NewGrabDownloader() GrabDownloader {
|
||||
client := grab.Client{
|
||||
UserAgent: "Cloud-Game/2.2",
|
||||
HTTPClient: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
return GrabDownloader{
|
||||
client: &client,
|
||||
concurrency: 5,
|
||||
}
|
||||
}
|
||||
|
||||
func (d GrabDownloader) Request(dest string, urls ...Download) (ok []string, nook []string) {
|
||||
reqs := make([]*grab.Request, 0)
|
||||
for _, url := range urls {
|
||||
req, err := grab.NewRequest(dest, url.Address)
|
||||
if err != nil {
|
||||
log.Printf("error: couldn't make request URL: %v, %v", url, err)
|
||||
} else {
|
||||
req.Label = url.Key
|
||||
reqs = append(reqs, req)
|
||||
}
|
||||
}
|
||||
|
||||
// check each response
|
||||
for resp := range d.client.DoBatch(d.concurrency, reqs...) {
|
||||
r := resp.Request
|
||||
if err := resp.Err(); err != nil {
|
||||
log.Printf("error: download [%s] %s failed: %v\n", r.Label, r.URL(), err)
|
||||
if resp.HTTPResponse == nil || resp.HTTPResponse.StatusCode == 404 {
|
||||
nook = append(nook, resp.Request.Label)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Downloaded [%v] [%s] %v -> %s", resp.HTTPResponse.Status, r.Label, r.URL(), resp.Filename)
|
||||
ok = append(ok, resp.Filename)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
package downloader
|
||||
|
||||
import (
|
||||
"github.com/giongto35/cloud-game/v2/pkg/downloader/backend"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/downloader/pipe"
|
||||
)
|
||||
|
||||
type Downloader struct {
|
||||
backend backend.Client
|
||||
// pipe contains a sequential list of
|
||||
// operations applied to some files and
|
||||
// each operation will return a list of
|
||||
// successfully processed files
|
||||
pipe []Process
|
||||
}
|
||||
|
||||
|
||||
type Process func(string, []string) []string
|
||||
|
||||
func NewDefaultDownloader() Downloader {
|
||||
return Downloader{
|
||||
backend: backend.NewGrabDownloader(),
|
||||
pipe: []Process{
|
||||
pipe.Unpack,
|
||||
pipe.Delete,
|
||||
}}
|
||||
}
|
||||
|
||||
// Download tries to download specified with URLs list of files and
|
||||
// put them into the destination folder.
|
||||
// It will return a partial or full list of downloaded files,
|
||||
// a list of processed files if some pipe processing functions are set.
|
||||
func (d *Downloader) Download(dest string, urls ...backend.Download) ([]string, []string) {
|
||||
files, fails := d.backend.Request(dest, urls...)
|
||||
for _, op := range d.pipe {
|
||||
files = op(dest, files)
|
||||
}
|
||||
return files, fails
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package pipe
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/compression"
|
||||
)
|
||||
|
||||
func Unpack(dest string, files []string) []string {
|
||||
var res []string
|
||||
for _, file := range files {
|
||||
if unpack := compression.NewExtractorFromExt(file); unpack != nil {
|
||||
if _, err := unpack.Extract(file, dest); err == nil {
|
||||
res = append(res, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func Delete(_ string, files []string) []string {
|
||||
var res []string
|
||||
for _, file := range files {
|
||||
if e := os.Remove(file); e == nil {
|
||||
res = append(res, file)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package emulator
|
||||
|
||||
import "github.com/giongto35/cloud-game/v2/pkg/emulator/image"
|
||||
|
||||
// CloudEmulator is the interface of cloud emulator.
|
||||
type CloudEmulator interface {
|
||||
// LoadMeta returns meta data of emulator. Refer below
|
||||
LoadMeta(path string) Metadata
|
||||
// Start is called after LoadGame
|
||||
Start()
|
||||
// SetViewport sets viewport size
|
||||
SetViewport(width int, height int)
|
||||
// SaveGame save game state
|
||||
SaveGame() error
|
||||
// LoadGame load game state
|
||||
LoadGame() error
|
||||
// GetHashPath returns the path emulator will save state to
|
||||
GetHashPath() string
|
||||
// Close will be called when the game is done
|
||||
Close()
|
||||
|
||||
ToggleMultitap() error
|
||||
}
|
||||
|
||||
type Metadata struct {
|
||||
// the full path to some emulator lib
|
||||
LibPath string
|
||||
// the full path to the emulator config
|
||||
ConfigPath string
|
||||
|
||||
AudioSampleRate int
|
||||
Fps float64
|
||||
BaseWidth int
|
||||
BaseHeight int
|
||||
Ratio float64
|
||||
Rotation image.Rotate
|
||||
IsGlAllowed bool
|
||||
UsesLibCo bool
|
||||
AutoGlContext bool
|
||||
HasMultitap bool
|
||||
}
|
||||
|
|
@ -1,140 +0,0 @@
|
|||
package graphics
|
||||
|
||||
import (
|
||||
"log"
|
||||
"unsafe"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/emulator/backend/gl"
|
||||
)
|
||||
|
||||
type offscreenSetup struct {
|
||||
tex uint32
|
||||
fbo uint32
|
||||
rbo uint32
|
||||
|
||||
width int32
|
||||
height int32
|
||||
|
||||
pixType uint32
|
||||
pixFormat uint32
|
||||
|
||||
hasDepth bool
|
||||
hasStencil bool
|
||||
}
|
||||
|
||||
var (
|
||||
opt = offscreenSetup{}
|
||||
buf []byte
|
||||
)
|
||||
|
||||
type PixelFormat int
|
||||
|
||||
const (
|
||||
UnsignedShort5551 PixelFormat = iota
|
||||
UnsignedShort565
|
||||
UnsignedInt8888Rev
|
||||
)
|
||||
|
||||
func initContext(getProcAddr func(name string) unsafe.Pointer) {
|
||||
if err := gl.InitWithProcAddrFunc(getProcAddr); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func initFramebuffer(w int, h int, hasDepth bool, hasStencil bool) {
|
||||
opt.width = int32(w)
|
||||
opt.height = int32(h)
|
||||
opt.hasDepth = hasDepth
|
||||
opt.hasStencil = hasStencil
|
||||
|
||||
// texture init
|
||||
gl.GenTextures(1, &opt.tex)
|
||||
gl.BindTexture(gl.TEXTURE_2D, opt.tex)
|
||||
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
|
||||
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
|
||||
|
||||
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, opt.width, opt.height, 0, opt.pixType, opt.pixFormat, nil)
|
||||
gl.BindTexture(gl.TEXTURE_2D, 0)
|
||||
|
||||
// framebuffer init
|
||||
gl.GenFramebuffers(1, &opt.fbo)
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo)
|
||||
|
||||
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, opt.tex, 0)
|
||||
|
||||
// more buffers init
|
||||
if opt.hasDepth {
|
||||
gl.GenRenderbuffers(1, &opt.rbo)
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, opt.rbo)
|
||||
if opt.hasStencil {
|
||||
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, opt.width, opt.height)
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, opt.rbo)
|
||||
} else {
|
||||
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, opt.width, opt.height)
|
||||
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, opt.rbo)
|
||||
}
|
||||
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
|
||||
}
|
||||
|
||||
status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER)
|
||||
if status != gl.FRAMEBUFFER_COMPLETE {
|
||||
if e := gl.GetError(); e != gl.NO_ERROR {
|
||||
log.Printf("[OpenGL] GL error: 0x%X, Frame status: 0x%X", e, status)
|
||||
panic("OpenGL error")
|
||||
}
|
||||
log.Printf("[OpenGL] frame status: 0x%X", status)
|
||||
panic("OpenGL framebuffer is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func destroyFramebuffer() {
|
||||
if opt.hasDepth {
|
||||
gl.DeleteRenderbuffers(1, &opt.rbo)
|
||||
}
|
||||
gl.DeleteFramebuffers(1, &opt.fbo)
|
||||
gl.DeleteTextures(1, &opt.tex)
|
||||
}
|
||||
|
||||
func ReadFramebuffer(bytes int, w int, h int) []byte {
|
||||
data := buf[:bytes]
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, opt.fbo)
|
||||
gl.ReadPixels(0, 0, int32(w), int32(h), opt.pixType, opt.pixFormat, unsafe.Pointer(&data[0]))
|
||||
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
|
||||
return data
|
||||
}
|
||||
|
||||
func getFbo() uint32 { return opt.fbo }
|
||||
|
||||
func SetBuffer(size int) { buf = make([]byte, size) }
|
||||
|
||||
func SetPixelFormat(format PixelFormat) {
|
||||
switch format {
|
||||
case UnsignedShort5551:
|
||||
opt.pixFormat = gl.UNSIGNED_SHORT_5_5_5_1
|
||||
opt.pixType = gl.BGRA
|
||||
case UnsignedShort565:
|
||||
opt.pixFormat = gl.UNSIGNED_SHORT_5_6_5
|
||||
opt.pixType = gl.RGB
|
||||
case UnsignedInt8888Rev:
|
||||
opt.pixFormat = gl.UNSIGNED_INT_8_8_8_8_REV
|
||||
opt.pixType = gl.BGRA
|
||||
default:
|
||||
log.Fatalf("[opengl] Error! Unknown pixel type %v", format)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintDriverInfo prints OpenGL information.
|
||||
func PrintDriverInfo() {
|
||||
// OpenGL info
|
||||
log.Printf("[OpenGL] Version: %v", get(gl.VERSION))
|
||||
log.Printf("[OpenGL] Vendor: %v", get(gl.VENDOR))
|
||||
// This string is often the name of the GPU.
|
||||
// In the case of Mesa3d, it would be i.e "Gallium 0.4 on NVA8".
|
||||
// It might even say "Direct3D" if the Windows Direct3D wrapper is being used.
|
||||
log.Printf("[OpenGL] Renderer: %v", get(gl.RENDERER))
|
||||
log.Printf("[OpenGL] GLSL Version: %v", get(gl.SHADING_LANGUAGE_VERSION))
|
||||
}
|
||||
|
||||
func get(name uint32) string { return gl.GoStr(gl.GetString(name)) }
|
||||
func getDriverError() uint32 { return gl.GetError() }
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
package graphics
|
||||
|
||||
import (
|
||||
"log"
|
||||
"unsafe"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/thread"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
)
|
||||
|
||||
type data struct {
|
||||
w *sdl.Window
|
||||
glWCtx sdl.GLContext
|
||||
}
|
||||
|
||||
// singleton state for SDL
|
||||
var state = data{}
|
||||
|
||||
type Config struct {
|
||||
Ctx Context
|
||||
W int
|
||||
H int
|
||||
Gl GlConfig
|
||||
}
|
||||
type GlConfig struct {
|
||||
AutoContext bool
|
||||
VersionMajor uint
|
||||
VersionMinor uint
|
||||
HasDepth bool
|
||||
HasStencil bool
|
||||
}
|
||||
|
||||
// Init initializes SDL/OpenGL context.
|
||||
// Uses main thread lock (see thread/mainthread).
|
||||
func Init(cfg Config) {
|
||||
log.Printf("[SDL] [OpenGL] initialization...")
|
||||
if err := sdl.Init(sdl.INIT_VIDEO); err != nil {
|
||||
log.Printf("[SDL] error: %v", err)
|
||||
panic("SDL initialization failed")
|
||||
}
|
||||
|
||||
if cfg.Gl.AutoContext {
|
||||
log.Printf("[OpenGL] CONTEXT_AUTO (type: %v v%v.%v)", cfg.Ctx, cfg.Gl.VersionMajor, cfg.Gl.VersionMinor)
|
||||
} else {
|
||||
switch cfg.Ctx {
|
||||
case CtxOpenGlCore:
|
||||
setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
|
||||
log.Printf("[OpenGL] CONTEXT_PROFILE_CORE")
|
||||
case CtxOpenGlEs2:
|
||||
setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_ES)
|
||||
setAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, 3)
|
||||
setAttribute(sdl.GL_CONTEXT_MINOR_VERSION, 0)
|
||||
log.Printf("[OpenGL] CONTEXT_PROFILE_ES 3.0")
|
||||
case CtxOpenGl:
|
||||
if cfg.Gl.VersionMajor >= 3 {
|
||||
setAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_COMPATIBILITY)
|
||||
}
|
||||
log.Printf("[OpenGL] CONTEXT_PROFILE_COMPATIBILITY")
|
||||
default:
|
||||
log.Printf("Unsupported hw context: %v", cfg.Ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// In OSX 10.14+ window creation and context creation must happen in the main thread
|
||||
thread.Main(createWindow)
|
||||
|
||||
BindContext()
|
||||
|
||||
initContext(sdl.GLGetProcAddress)
|
||||
PrintDriverInfo()
|
||||
initFramebuffer(cfg.W, cfg.H, cfg.Gl.HasDepth, cfg.Gl.HasStencil)
|
||||
}
|
||||
|
||||
// Deinit destroys SDL/OpenGL context.
|
||||
// Uses main thread lock (see thread/mainthread).
|
||||
func Deinit() {
|
||||
log.Printf("[SDL] [OpenGL] deinitialization...")
|
||||
destroyFramebuffer()
|
||||
// In OSX 10.14+ window deletion must happen in the main thread
|
||||
thread.Main(destroyWindow)
|
||||
sdl.Quit()
|
||||
log.Printf("[SDL] [OpenGL] deinitialized (%v, %v)", sdl.GetError(), getDriverError())
|
||||
}
|
||||
|
||||
// createWindow creates fake SDL window for OpenGL initialization purposes.
|
||||
func createWindow() {
|
||||
var winTitle = "CloudRetro dummy window"
|
||||
var winWidth, winHeight int32 = 1, 1
|
||||
|
||||
var err error
|
||||
if state.w, err = sdl.CreateWindow(
|
||||
winTitle,
|
||||
sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED,
|
||||
winWidth, winHeight,
|
||||
sdl.WINDOW_OPENGL|sdl.WINDOW_HIDDEN,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if state.glWCtx, err = state.w.GLCreateContext(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// destroyWindow destroys previously created SDL window.
|
||||
func destroyWindow() {
|
||||
BindContext()
|
||||
sdl.GLDeleteContext(state.glWCtx)
|
||||
if err := state.w.Destroy(); err != nil {
|
||||
log.Printf("[SDL] couldn't destroy the window, error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// BindContext explicitly binds context to current thread.
|
||||
func BindContext() {
|
||||
if err := state.w.GLMakeCurrent(state.glWCtx); err != nil {
|
||||
log.Printf("[SDL] error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetGlFbo() uint32 {
|
||||
return getFbo()
|
||||
}
|
||||
|
||||
func GetGlProcAddress(proc string) unsafe.Pointer {
|
||||
return sdl.GLGetProcAddress(proc)
|
||||
}
|
||||
|
||||
func setAttribute(attr sdl.GLattr, value int) {
|
||||
if err := sdl.GLSetAttribute(attr, value); err != nil {
|
||||
log.Printf("[SDL] attribute error: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package image
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const (
|
||||
// BIT_FORMAT_SHORT_5_5_5_1 has 5 bits R, 5 bits G, 5 bits B, 1 bit alpha
|
||||
BitFormatShort5551 = iota
|
||||
// BIT_FORMAT_INT_8_8_8_8_REV has 8 bits R, 8 bits G, 8 bits B, 8 bit alpha
|
||||
BitFormatInt8888Rev
|
||||
// BIT_FORMAT_SHORT_5_6_5 has 5 bits R, 6 bits G, 5 bits
|
||||
BitFormatShort565
|
||||
)
|
||||
|
||||
type RGB struct {
|
||||
R, G, B uint8
|
||||
}
|
||||
|
||||
type Format func(data []byte, index int) RGB
|
||||
|
||||
func Rgb565(data []byte, index int) RGB {
|
||||
pixel := *(*uint16)(unsafe.Pointer(&data[index]))
|
||||
return RGB{R: uint8((pixel >> 8) & 0xf8), G: uint8((pixel >> 3) & 0xfc), B: uint8((pixel << 3) & 0xfc)}
|
||||
}
|
||||
|
||||
func Rgba8888(data []byte, index int) RGB {
|
||||
return RGB{R: data[index+2], G: data[index+1], B: data[index]}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
package image
|
||||
|
||||
import (
|
||||
"image"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type imageCache struct {
|
||||
image *image.RGBA
|
||||
w int
|
||||
h int
|
||||
}
|
||||
|
||||
func (i *imageCache) get(w, h int) *image.RGBA {
|
||||
if i.w == w && i.h == h {
|
||||
return i.image
|
||||
}
|
||||
i.w, i.h = w, h
|
||||
i.image = image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
return i.image
|
||||
}
|
||||
|
||||
var (
|
||||
canvas1 = imageCache{image.NewRGBA(image.Rectangle{}), 0, 0}
|
||||
canvas2 = imageCache{image.NewRGBA(image.Rectangle{}), 0, 0}
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
|
||||
func DrawRgbaImage(pixFormat Format, rot *Rotate, scaleType int, flipV bool, w, h, packedW, bpp int,
|
||||
data []byte, dw, dh, th int) *image.RGBA {
|
||||
// !to implement own image interfaces img.Pix = bytes[]
|
||||
ww, hh := w, h
|
||||
if rot != nil && rot.IsEven {
|
||||
ww, hh = hh, ww
|
||||
}
|
||||
src := canvas1.get(ww, hh)
|
||||
|
||||
normY := !flipV
|
||||
hn := h / th
|
||||
pwb := packedW * bpp
|
||||
wg.Add(th)
|
||||
for i := 0; i < th; i++ {
|
||||
xx := hn * i
|
||||
go func() {
|
||||
for y, yy, l, lx, row := xx, 0, xx+hn, 0, 0; y < l; y++ {
|
||||
if normY {
|
||||
yy = y
|
||||
} else {
|
||||
yy = (h - 1) - y
|
||||
}
|
||||
row = yy * src.Stride
|
||||
lx = y * pwb
|
||||
for x, k := 0, 0; x < w; x++ {
|
||||
if rot == nil {
|
||||
k = x<<2 + row
|
||||
} else {
|
||||
dx, dy := rot.Call(x, yy, w, h)
|
||||
k = dx<<2 + dy*src.Stride
|
||||
}
|
||||
r := pixFormat(data, x*bpp+lx)
|
||||
src.Pix[k], src.Pix[k+1], src.Pix[k+2], src.Pix[k+3] = r.R, r.G, r.B, 255
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
if ww == dw && hh == dh {
|
||||
return src
|
||||
} else {
|
||||
out := canvas2.get(dw, dh)
|
||||
Resize(scaleType, src, out)
|
||||
return out
|
||||
}
|
||||
}
|
||||
|
||||
func Clear() {
|
||||
wg = sync.WaitGroup{}
|
||||
canvas1.get(0, 0)
|
||||
canvas2.get(0, 0)
|
||||
}
|
||||
|
|
@ -1,204 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include <pthread.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
|
||||
void coreLog(enum retro_log_level level, const char *msg);
|
||||
|
||||
void bridge_retro_init(void *f) {
|
||||
coreLog(RETRO_LOG_INFO, "[Libretro] Initialization...\n");
|
||||
return ((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_deinit(void *f) {
|
||||
coreLog(RETRO_LOG_INFO, "[Libretro] Deinitialiazation...\n");
|
||||
return ((void (*)(void))f)();
|
||||
}
|
||||
|
||||
unsigned bridge_retro_api_version(void *f) {
|
||||
return ((unsigned (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_get_system_info(void *f, struct retro_system_info *si) {
|
||||
return ((void (*)(struct retro_system_info *))f)(si);
|
||||
}
|
||||
|
||||
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si) {
|
||||
return ((void (*)(struct retro_system_av_info *))f)(si);
|
||||
}
|
||||
|
||||
bool bridge_retro_set_environment(void *f, void *callback) {
|
||||
return ((bool (*)(retro_environment_t))f)((retro_environment_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback) {
|
||||
((bool (*)(retro_video_refresh_t))f)((retro_video_refresh_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_poll(void *f, void *callback) {
|
||||
((bool (*)(retro_input_poll_t))f)((retro_input_poll_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_input_state(void *f, void *callback) {
|
||||
((bool (*)(retro_input_state_t))f)((retro_input_state_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_t))f)((retro_audio_sample_t)callback);
|
||||
}
|
||||
|
||||
void bridge_retro_set_audio_sample_batch(void *f, void *callback) {
|
||||
((bool (*)(retro_audio_sample_batch_t))f)((retro_audio_sample_batch_t)callback);
|
||||
}
|
||||
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi) {
|
||||
coreLog(RETRO_LOG_INFO, "[Libretro] Loading the game...\n");
|
||||
return ((bool (*)(struct retro_game_info *))f)(gi);
|
||||
}
|
||||
|
||||
void bridge_retro_unload_game(void *f) {
|
||||
coreLog(RETRO_LOG_INFO, "[Libretro] Unloading the game...\n");
|
||||
return ((void (*)(void))f)();
|
||||
}
|
||||
|
||||
void bridge_retro_run(void *f) {
|
||||
return ((void (*)(void))f)();
|
||||
}
|
||||
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id) {
|
||||
return ((size_t (*)(unsigned))f)(id);
|
||||
}
|
||||
|
||||
void* bridge_retro_get_memory_data(void *f, unsigned id) {
|
||||
return ((void* (*)(unsigned))f)(id);
|
||||
}
|
||||
|
||||
size_t bridge_retro_serialize_size(void *f) {
|
||||
return ((size_t (*)(void))f)();
|
||||
}
|
||||
|
||||
bool bridge_retro_serialize(void *f, void *data, size_t size) {
|
||||
return ((bool (*)(void*, size_t))f)(data, size);
|
||||
}
|
||||
|
||||
bool bridge_retro_unserialize(void *f, void *data, size_t size) {
|
||||
return ((bool (*)(void*, size_t))f)(data, size);
|
||||
}
|
||||
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device) {
|
||||
return ((void (*)(unsigned, unsigned))f)(port, device);
|
||||
}
|
||||
|
||||
bool coreEnvironment_cgo(unsigned cmd, void *data) {
|
||||
bool coreEnvironment(unsigned, void*);
|
||||
return coreEnvironment(cmd, data);
|
||||
}
|
||||
|
||||
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch) {
|
||||
void coreVideoRefresh(void*, unsigned, unsigned, size_t);
|
||||
return coreVideoRefresh(data, width, height, pitch);
|
||||
}
|
||||
|
||||
void coreInputPoll_cgo() {
|
||||
void coreInputPoll();
|
||||
return coreInputPoll();
|
||||
}
|
||||
|
||||
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id) {
|
||||
int16_t coreInputState(unsigned, unsigned, unsigned, unsigned);
|
||||
return coreInputState(port, device, index, id);
|
||||
}
|
||||
|
||||
void coreAudioSample_cgo(int16_t left, int16_t right) {
|
||||
void coreAudioSample(int16_t, int16_t);
|
||||
coreAudioSample(left, right);
|
||||
}
|
||||
|
||||
size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames) {
|
||||
size_t coreAudioSampleBatch(const int16_t*, size_t);
|
||||
return coreAudioSampleBatch(data, frames);
|
||||
}
|
||||
|
||||
void coreLog_cgo(enum retro_log_level level, const char *fmt, ...) {
|
||||
char msg[4096] = {0};
|
||||
va_list va;
|
||||
va_start(va, fmt);
|
||||
vsnprintf(msg, sizeof(msg), fmt, va);
|
||||
va_end(va);
|
||||
|
||||
coreLog(level, msg);
|
||||
}
|
||||
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo() {
|
||||
uintptr_t coreGetCurrentFramebuffer();
|
||||
return coreGetCurrentFramebuffer();
|
||||
}
|
||||
|
||||
retro_proc_address_t coreGetProcAddress_cgo(const char *sym) {
|
||||
retro_proc_address_t coreGetProcAddress(const char *sym);
|
||||
return coreGetProcAddress(sym);
|
||||
}
|
||||
|
||||
void bridge_context_reset(retro_hw_context_reset_t f) {
|
||||
f();
|
||||
}
|
||||
|
||||
void initVideo_cgo() {
|
||||
void initVideo();
|
||||
return initVideo();
|
||||
}
|
||||
|
||||
void deinitVideo_cgo() {
|
||||
void deinitVideo();
|
||||
return deinitVideo();
|
||||
}
|
||||
|
||||
void* function;
|
||||
pthread_t thread;
|
||||
int initialized = 0;
|
||||
pthread_mutex_t run_mutex;
|
||||
pthread_cond_t run_cv;
|
||||
pthread_mutex_t done_mutex;
|
||||
pthread_cond_t done_cv;
|
||||
|
||||
void *run_loop(void *unused) {
|
||||
pthread_mutex_lock(&done_mutex);
|
||||
pthread_mutex_lock(&run_mutex);
|
||||
pthread_cond_signal(&done_cv);
|
||||
pthread_mutex_unlock(&done_mutex);
|
||||
while(1) {
|
||||
pthread_cond_wait(&run_cv, &run_mutex);
|
||||
((void (*)(void))function)();
|
||||
pthread_mutex_lock(&done_mutex);
|
||||
pthread_cond_signal(&done_cv);
|
||||
pthread_mutex_unlock(&done_mutex);
|
||||
}
|
||||
pthread_mutex_unlock(&run_mutex);
|
||||
}
|
||||
|
||||
void bridge_execute(void *f) {
|
||||
if (!initialized) {
|
||||
initialized = 1;
|
||||
pthread_mutex_init(&run_mutex, NULL);
|
||||
pthread_cond_init(&run_cv, NULL);
|
||||
pthread_mutex_init(&done_mutex, NULL);
|
||||
pthread_cond_init(&done_cv, NULL);
|
||||
pthread_mutex_lock(&done_mutex);
|
||||
pthread_create(&thread, NULL, run_loop, NULL);
|
||||
pthread_cond_wait(&done_cv, &done_mutex);
|
||||
pthread_mutex_unlock(&done_mutex);
|
||||
}
|
||||
pthread_mutex_lock(&run_mutex);
|
||||
pthread_mutex_lock(&done_mutex);
|
||||
function = f;
|
||||
pthread_cond_signal(&run_cv);
|
||||
pthread_mutex_unlock(&run_mutex);
|
||||
pthread_cond_wait(&done_cv, &done_mutex);
|
||||
pthread_mutex_unlock(&done_mutex);
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
import "C"
|
||||
|
||||
type ConfigProperties map[string]*C.char
|
||||
|
||||
func ScanConfigFile(filename string) ConfigProperties {
|
||||
config := ConfigProperties{}
|
||||
|
||||
if len(filename) == 0 {
|
||||
return config
|
||||
}
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
log.Printf("warning: couldn't find the %v config file", filename)
|
||||
return config
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if equal := strings.Index(line, "="); equal >= 0 {
|
||||
if key := strings.TrimSpace(line[:equal]); len(key) > 0 {
|
||||
value := ""
|
||||
if len(line) > equal {
|
||||
value = strings.TrimSpace(line[equal+1:])
|
||||
}
|
||||
config[key] = C.CString(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
import "sync"
|
||||
|
||||
const (
|
||||
// how many axes on the D-pad
|
||||
dpadAxesNum = 4
|
||||
// the upper limit on how many controllers (players)
|
||||
// are possible for one play session (emulator instance)
|
||||
controllersNum = 8
|
||||
)
|
||||
|
||||
const (
|
||||
InputTerminate = 0xFFFF
|
||||
)
|
||||
|
||||
type Players struct {
|
||||
session playerSession
|
||||
}
|
||||
|
||||
type playerSession struct {
|
||||
sync.RWMutex
|
||||
|
||||
state map[string][]controllerState
|
||||
}
|
||||
|
||||
type controllerState struct {
|
||||
keyState uint16
|
||||
axes [dpadAxesNum]int16
|
||||
}
|
||||
|
||||
func NewPlayerSessionInput() Players {
|
||||
return Players{
|
||||
session: playerSession{
|
||||
state: map[string][]controllerState{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// close terminates user input session.
|
||||
func (ps *playerSession) close(id string) {
|
||||
ps.Lock()
|
||||
defer ps.Unlock()
|
||||
|
||||
delete(ps.state, id)
|
||||
}
|
||||
|
||||
// setInput sets input state for some player in a game session.
|
||||
func (ps *playerSession) setInput(id string, player int, buttons uint16, dpad []byte) {
|
||||
ps.Lock()
|
||||
defer ps.Unlock()
|
||||
|
||||
if _, ok := ps.state[id]; !ok {
|
||||
ps.state[id] = make([]controllerState, controllersNum)
|
||||
}
|
||||
|
||||
ps.state[id][player].keyState = buttons
|
||||
for i, axes := 0, len(dpad); i < dpadAxesNum && (i+1)*2+1 < axes; i++ {
|
||||
axis := (i + 1) * 2
|
||||
ps.state[id][player].axes[i] = int16(dpad[axis+1])<<8 + int16(dpad[axis])
|
||||
}
|
||||
}
|
||||
|
||||
// isKeyPressed checks if some button is pressed by any player.
|
||||
func (p *Players) isKeyPressed(player uint, key int) (pressed bool) {
|
||||
p.session.RLock()
|
||||
defer p.session.RUnlock()
|
||||
|
||||
for k := range p.session.state {
|
||||
if ((p.session.state[k][player].keyState >> uint(key)) & 1) == 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// isDpadTouched checks if D-pad is used by any player.
|
||||
func (p *Players) isDpadTouched(player uint, axis uint) (shift int16) {
|
||||
p.session.RLock()
|
||||
defer p.session.RUnlock()
|
||||
|
||||
for k := range p.session.state {
|
||||
value := p.session.state[k][player].axes[axis]
|
||||
if value != 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type InputEvent struct {
|
||||
RawState []byte
|
||||
PlayerIdx int
|
||||
ConnID string
|
||||
}
|
||||
|
||||
func (ie InputEvent) bitmap() uint16 { return uint16(ie.RawState[1])<<8 + uint16(ie.RawState[0]) }
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConcurrentInput(t *testing.T) {
|
||||
players := NewPlayerSessionInput()
|
||||
|
||||
session := "mad-test-session"
|
||||
events := 1000
|
||||
go func() {
|
||||
for i := 0; i < events*2; i++ {
|
||||
player := rand.Intn(controllersNum)
|
||||
go players.session.setInput(session, player, 100, []byte{})
|
||||
// here it usually crashes
|
||||
go players.session.close(session)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
for i := 0; i < events*2; i++ {
|
||||
player := rand.Intn(controllersNum)
|
||||
go players.isKeyPressed(uint(player), 100)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"image"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
config "github.com/giongto35/cloud-game/v2/pkg/config/emulator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/emulator"
|
||||
)
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#cgo LDFLAGS: -ldl
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <dlfcn.h>
|
||||
#include <string.h>
|
||||
|
||||
void bridge_retro_deinit(void *f);
|
||||
unsigned bridge_retro_api_version(void *f);
|
||||
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
|
||||
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
|
||||
bool bridge_retro_set_environment(void *f, void *callback);
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback);
|
||||
void bridge_retro_set_input_poll(void *f, void *callback);
|
||||
void bridge_retro_set_input_state(void *f, void *callback);
|
||||
void bridge_retro_set_audio_sample(void *f, void *callback);
|
||||
void bridge_retro_set_audio_sample_batch(void *f, void *callback);
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi);
|
||||
void bridge_retro_run(void *f);
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id);
|
||||
void* bridge_retro_get_memory_data(void *f, unsigned id);
|
||||
bool bridge_retro_serialize(void *f, void *data, size_t size);
|
||||
bool bridge_retro_unserialize(void *f, void *data, size_t size);
|
||||
size_t bridge_retro_serialize_size(void *f);
|
||||
|
||||
bool coreEnvironment_cgo(unsigned cmd, void *data);
|
||||
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
|
||||
void coreInputPoll_cgo();
|
||||
void coreAudioSample_cgo(int16_t left, int16_t right);
|
||||
size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
|
||||
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
void coreLog_cgo(enum retro_log_level level, const char *msg);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type naEmulator struct {
|
||||
sync.Mutex
|
||||
|
||||
imageChannel chan<- GameFrame
|
||||
audioChannel chan<- []int16
|
||||
inputChannel <-chan InputEvent
|
||||
videoExporter *VideoExporter
|
||||
|
||||
meta emulator.Metadata
|
||||
gamePath string
|
||||
roomID string
|
||||
gameName string
|
||||
isSavingLoading bool
|
||||
storage Storage
|
||||
saveCompression bool
|
||||
|
||||
// out frame size
|
||||
vw, vh int
|
||||
|
||||
// draw threads
|
||||
th int
|
||||
|
||||
players Players
|
||||
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// VideoExporter produces image frame to unix socket
|
||||
type VideoExporter struct {
|
||||
sock net.Conn
|
||||
imageChannel chan<- GameFrame
|
||||
}
|
||||
|
||||
// GameFrame contains image and timeframe
|
||||
type GameFrame struct {
|
||||
Data *image.RGBA
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
var NAEmulator *naEmulator
|
||||
|
||||
// NAEmulator implements CloudEmulator interface based on NanoArch(golang RetroArch)
|
||||
func NewNAEmulator(roomID string, inputChannel <-chan InputEvent, storage Storage, conf config.LibretroCoreConfig, threads int) (*naEmulator, chan GameFrame, chan []int16) {
|
||||
imageChannel := make(chan GameFrame, 30)
|
||||
audioChannel := make(chan []int16, 30)
|
||||
|
||||
return &naEmulator{
|
||||
meta: emulator.Metadata{
|
||||
LibPath: conf.Lib,
|
||||
ConfigPath: conf.Config,
|
||||
Ratio: conf.Ratio,
|
||||
IsGlAllowed: conf.IsGlAllowed,
|
||||
UsesLibCo: conf.UsesLibCo,
|
||||
HasMultitap: conf.HasMultitap,
|
||||
AutoGlContext: conf.AutoGlContext,
|
||||
},
|
||||
storage: storage,
|
||||
imageChannel: imageChannel,
|
||||
audioChannel: audioChannel,
|
||||
inputChannel: inputChannel,
|
||||
players: NewPlayerSessionInput(),
|
||||
roomID: roomID,
|
||||
done: make(chan struct{}, 1),
|
||||
th: threads,
|
||||
}, imageChannel, audioChannel
|
||||
}
|
||||
|
||||
// NewVideoExporter creates new video Exporter that produces to unix socket
|
||||
func NewVideoExporter(roomID string, imgChannel chan GameFrame) *VideoExporter {
|
||||
sockAddr := fmt.Sprintf("/tmp/cloudretro-retro-%s.sock", roomID)
|
||||
|
||||
go func(sockAddr string) {
|
||||
log.Println("Dialing to ", sockAddr)
|
||||
conn, err := net.Dial("unix", sockAddr)
|
||||
if err != nil {
|
||||
log.Fatal("accept error: ", err)
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
|
||||
for img := range imgChannel {
|
||||
reqBodyBytes := new(bytes.Buffer)
|
||||
gob.NewEncoder(reqBodyBytes).Encode(img)
|
||||
//fmt.Printf("%+v %+v %+v \n", img.Image.Stride, img.Image.Rect.Max.X, len(img.Image.Pix))
|
||||
// conn.Write(img.Image.Pix)
|
||||
b := reqBodyBytes.Bytes()
|
||||
fmt.Printf("Bytes %d\n", len(b))
|
||||
conn.Write(b)
|
||||
}
|
||||
}(sockAddr)
|
||||
|
||||
return &VideoExporter{imageChannel: imgChannel}
|
||||
}
|
||||
|
||||
// Init initialize new RetroArch cloud emulator
|
||||
// withImageChan returns an image stream as Channel for output else it will write to unix socket
|
||||
func Init(roomID string, withImageChannel bool, inputChannel <-chan InputEvent, storage Storage, config config.LibretroCoreConfig, threads int) (*naEmulator, chan GameFrame, chan []int16) {
|
||||
emu, imageChannel, audioChannel := NewNAEmulator(roomID, inputChannel, storage, config, threads)
|
||||
// Set to global NAEmulator
|
||||
NAEmulator = emu
|
||||
if !withImageChannel {
|
||||
NAEmulator.videoExporter = NewVideoExporter(roomID, imageChannel)
|
||||
}
|
||||
|
||||
go NAEmulator.listenInput()
|
||||
|
||||
return emu, imageChannel, audioChannel
|
||||
}
|
||||
|
||||
// listenInput handles user input.
|
||||
// The user input is encoded as bitmap that we decode
|
||||
// and send into the game emulator.
|
||||
func (na *naEmulator) listenInput() {
|
||||
for in := range NAEmulator.inputChannel {
|
||||
bitmap := in.bitmap()
|
||||
if bitmap == InputTerminate {
|
||||
na.players.session.close(in.ConnID)
|
||||
continue
|
||||
}
|
||||
na.players.session.setInput(in.ConnID, in.PlayerIdx, bitmap, in.RawState)
|
||||
}
|
||||
}
|
||||
|
||||
func (na *naEmulator) LoadMeta(path string) emulator.Metadata {
|
||||
coreLoad(na.meta)
|
||||
coreLoadGame(path)
|
||||
na.gamePath = path
|
||||
return na.meta
|
||||
}
|
||||
|
||||
func (na *naEmulator) SetViewport(width int, height int) { na.vw, na.vh = width, height }
|
||||
|
||||
func (na *naEmulator) Start() {
|
||||
err := na.LoadGame()
|
||||
if err != nil {
|
||||
log.Printf("error: couldn't load a save, %v", err)
|
||||
}
|
||||
|
||||
framerate := 1 / na.meta.Fps
|
||||
log.Printf("framerate: %vms", framerate)
|
||||
ticker := time.NewTicker(time.Second / time.Duration(na.meta.Fps))
|
||||
defer ticker.Stop()
|
||||
|
||||
lastFrameTime = time.Now().UnixNano()
|
||||
|
||||
for {
|
||||
na.Lock()
|
||||
nanoarchRun()
|
||||
na.Unlock()
|
||||
|
||||
select {
|
||||
case <-ticker.C:
|
||||
continue
|
||||
case <-na.done:
|
||||
nanoarchShutdown()
|
||||
close(na.imageChannel)
|
||||
close(na.audioChannel)
|
||||
log.Println("Closed Director")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (na *naEmulator) SaveGame() error {
|
||||
// !to fix
|
||||
if usesLibCo {
|
||||
return nil
|
||||
}
|
||||
if na.roomID != "" {
|
||||
return na.Save()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (na *naEmulator) LoadGame() error {
|
||||
// !to fix
|
||||
if usesLibCo {
|
||||
return nil
|
||||
}
|
||||
if na.roomID != "" {
|
||||
err := na.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (na *naEmulator) ToggleMultitap() error {
|
||||
if na.roomID != "" {
|
||||
toggleMultitap()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (na *naEmulator) GetHashPath() string { return na.storage.GetSavePath() }
|
||||
|
||||
func (na *naEmulator) GetSRAMPath() string { return na.storage.GetSRAMPath() }
|
||||
|
||||
func (na *naEmulator) Close() { close(na.done) }
|
||||
|
|
@ -1,697 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/emulator"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/emulator/graphics"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/emulator/image"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/thread"
|
||||
)
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
void bridge_retro_init(void *f);
|
||||
void bridge_retro_deinit(void *f);
|
||||
unsigned bridge_retro_api_version(void *f);
|
||||
void bridge_retro_get_system_info(void *f, struct retro_system_info *si);
|
||||
void bridge_retro_get_system_av_info(void *f, struct retro_system_av_info *si);
|
||||
bool bridge_retro_set_environment(void *f, void *callback);
|
||||
void bridge_retro_set_video_refresh(void *f, void *callback);
|
||||
void bridge_retro_set_input_poll(void *f, void *callback);
|
||||
void bridge_retro_set_input_state(void *f, void *callback);
|
||||
void bridge_retro_set_audio_sample(void *f, void *callback);
|
||||
void bridge_retro_set_audio_sample_batch(void *f, void *callback);
|
||||
bool bridge_retro_load_game(void *f, struct retro_game_info *gi);
|
||||
void bridge_retro_unload_game(void *f);
|
||||
void bridge_retro_run(void *f);
|
||||
void bridge_retro_set_controller_port_device(void *f, unsigned port, unsigned device);
|
||||
|
||||
bool coreEnvironment_cgo(unsigned cmd, void *data);
|
||||
void coreVideoRefresh_cgo(void *data, unsigned width, unsigned height, size_t pitch);
|
||||
void coreInputPoll_cgo();
|
||||
void coreAudioSample_cgo(int16_t left, int16_t right);
|
||||
size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
|
||||
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
|
||||
void coreLog_cgo(enum retro_log_level level, const char *msg);
|
||||
uintptr_t coreGetCurrentFramebuffer_cgo();
|
||||
retro_proc_address_t coreGetProcAddress_cgo(const char *sym);
|
||||
|
||||
void bridge_context_reset(retro_hw_context_reset_t f);
|
||||
|
||||
void initVideo_cgo();
|
||||
void deinitVideo_cgo();
|
||||
void bridge_execute(void *f);
|
||||
*/
|
||||
import "C"
|
||||
|
||||
var mu sync.Mutex
|
||||
var lastFrameTime int64
|
||||
|
||||
var video struct {
|
||||
pitch uint32
|
||||
pixFmt uint32
|
||||
bpp uint32
|
||||
rotation image.Angle
|
||||
|
||||
baseWidth int32
|
||||
baseHeight int32
|
||||
maxWidth int32
|
||||
maxHeight int32
|
||||
|
||||
hw *C.struct_retro_hw_render_callback
|
||||
isGl bool
|
||||
autoGlContext bool
|
||||
}
|
||||
|
||||
// default core pix format converter
|
||||
var pixelFormatConverterFn = image.Rgb565
|
||||
var rotationFn *image.Rotate
|
||||
|
||||
//const joypadNumKeys = int(C.RETRO_DEVICE_ID_JOYPAD_R3 + 1)
|
||||
//var joy [joypadNumKeys]bool
|
||||
|
||||
var isGlAllowed bool
|
||||
var usesLibCo bool
|
||||
var coreConfig ConfigProperties
|
||||
|
||||
var multitap struct {
|
||||
supported bool
|
||||
enabled bool
|
||||
value C.unsigned
|
||||
}
|
||||
|
||||
var systemDirectory = C.CString("./pkg/emulator/libretro/system")
|
||||
var saveDirectory = C.CString(".")
|
||||
var currentUser *C.char
|
||||
|
||||
var bindKeysMap = map[int]int{
|
||||
C.RETRO_DEVICE_ID_JOYPAD_A: 0,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_B: 1,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_X: 2,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_Y: 3,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_L: 4,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_R: 5,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_SELECT: 6,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_START: 7,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_UP: 8,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_DOWN: 9,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_LEFT: 10,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_RIGHT: 11,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_R2: 12,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_L2: 13,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_R3: 14,
|
||||
C.RETRO_DEVICE_ID_JOYPAD_L3: 15,
|
||||
}
|
||||
|
||||
type CloudEmulator interface {
|
||||
Start(path string)
|
||||
SaveGame(saveExtraFunc func() error) error
|
||||
LoadGame() error
|
||||
GetHashPath() string
|
||||
Close()
|
||||
ToggleMultitap() error
|
||||
}
|
||||
|
||||
//export coreVideoRefresh
|
||||
func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned, pitch C.size_t) {
|
||||
// some cores can return nothing
|
||||
// !to add duplicate if can dup
|
||||
if data == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// calculate real frame width in pixels from packed data (realWidth >= width)
|
||||
packedWidth := int(uint32(pitch) / video.bpp)
|
||||
if packedWidth < 1 {
|
||||
packedWidth = int(width)
|
||||
}
|
||||
// calculate space for the video frame
|
||||
bytes := int(height) * packedWidth * int(video.bpp)
|
||||
|
||||
// if Libretro renders frame with OpenGL context
|
||||
isOpenGLRender := data == C.RETRO_HW_FRAME_BUFFER_VALID
|
||||
var data_ []byte
|
||||
if isOpenGLRender {
|
||||
data_ = graphics.ReadFramebuffer(bytes, int(width), int(height))
|
||||
} else {
|
||||
data_ = (*[1 << 30]byte)(data)[:bytes:bytes]
|
||||
}
|
||||
|
||||
// the image is being resized and de-rotated
|
||||
frame := image.DrawRgbaImage(
|
||||
pixelFormatConverterFn,
|
||||
rotationFn,
|
||||
image.ScaleNearestNeighbour,
|
||||
isOpenGLRender,
|
||||
int(width), int(height), packedWidth, int(video.bpp),
|
||||
data_,
|
||||
NAEmulator.vw,
|
||||
NAEmulator.vh,
|
||||
NAEmulator.th,
|
||||
)
|
||||
|
||||
t := time.Now().UnixNano()
|
||||
dt := time.Duration(t - lastFrameTime)
|
||||
lastFrameTime = t
|
||||
|
||||
select {
|
||||
case NAEmulator.imageChannel <- GameFrame{Data: frame, Duration: dt}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
//export coreInputPoll
|
||||
func coreInputPoll() {
|
||||
}
|
||||
|
||||
//export coreInputState
|
||||
func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t {
|
||||
if device == C.RETRO_DEVICE_ANALOG {
|
||||
if index > C.RETRO_DEVICE_INDEX_ANALOG_RIGHT || id > C.RETRO_DEVICE_ID_ANALOG_Y {
|
||||
return 0
|
||||
}
|
||||
axis := index*2 + id
|
||||
value := NAEmulator.players.isDpadTouched(uint(port), uint(axis))
|
||||
if value != 0 {
|
||||
return (C.int16_t)(value)
|
||||
}
|
||||
}
|
||||
|
||||
if id >= 255 || index > 0 || device != C.RETRO_DEVICE_JOYPAD {
|
||||
return 0
|
||||
}
|
||||
|
||||
// map from id to control key
|
||||
key, ok := bindKeysMap[int(id)]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
if NAEmulator.players.isKeyPressed(uint(port), key) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func audioWrite(buf unsafe.Pointer, frames C.size_t) C.size_t {
|
||||
samples := int(frames) << 1
|
||||
pcm := (*[4096]int16)(buf)[:samples:samples]
|
||||
p := make([]int16, samples)
|
||||
copy(p, pcm)
|
||||
|
||||
select {
|
||||
case NAEmulator.audioChannel <- p:
|
||||
default:
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
//export coreAudioSample
|
||||
func coreAudioSample(left C.int16_t, right C.int16_t) {
|
||||
buf := []C.int16_t{left, right}
|
||||
audioWrite(unsafe.Pointer(&buf), 1)
|
||||
}
|
||||
|
||||
//export coreAudioSampleBatch
|
||||
func coreAudioSampleBatch(data unsafe.Pointer, frames C.size_t) C.size_t {
|
||||
return audioWrite(data, frames)
|
||||
}
|
||||
|
||||
//export coreLog
|
||||
func coreLog(_ C.enum_retro_log_level, msg *C.char) {
|
||||
log.Printf("[Log] %v", C.GoString(msg))
|
||||
}
|
||||
|
||||
//export coreGetCurrentFramebuffer
|
||||
func coreGetCurrentFramebuffer() C.uintptr_t {
|
||||
return (C.uintptr_t)(graphics.GetGlFbo())
|
||||
}
|
||||
|
||||
//export coreGetProcAddress
|
||||
func coreGetProcAddress(sym *C.char) C.retro_proc_address_t {
|
||||
return (C.retro_proc_address_t)(graphics.GetGlProcAddress(C.GoString(sym)))
|
||||
}
|
||||
|
||||
//export coreEnvironment
|
||||
func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
|
||||
switch cmd {
|
||||
case C.RETRO_ENVIRONMENT_GET_USERNAME:
|
||||
username := (**C.char)(data)
|
||||
if currentUser == nil {
|
||||
currentUserGo, err := user.Current()
|
||||
if err != nil {
|
||||
currentUser = C.CString("")
|
||||
} else {
|
||||
currentUser = C.CString(currentUserGo.Username)
|
||||
}
|
||||
}
|
||||
*username = currentUser
|
||||
case C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
|
||||
cb := (*C.struct_retro_log_callback)(data)
|
||||
cb.log = (C.retro_log_printf_t)(C.coreLog_cgo)
|
||||
case C.RETRO_ENVIRONMENT_GET_CAN_DUPE:
|
||||
bval := (*C.bool)(data)
|
||||
*bval = C.bool(true)
|
||||
case C.RETRO_ENVIRONMENT_SET_PIXEL_FORMAT:
|
||||
return videoSetPixelFormat(*(*C.enum_retro_pixel_format)(data))
|
||||
case C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
|
||||
path := (**C.char)(data)
|
||||
*path = systemDirectory
|
||||
return true
|
||||
case C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
|
||||
path := (**C.char)(data)
|
||||
*path = saveDirectory
|
||||
return true
|
||||
case C.RETRO_ENVIRONMENT_SHUTDOWN:
|
||||
//window.SetShouldClose(true)
|
||||
return true
|
||||
/*
|
||||
Sets screen rotation of graphics.
|
||||
Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180, 270 degrees
|
||||
ccw respectively.
|
||||
*/
|
||||
case C.RETRO_ENVIRONMENT_SET_ROTATION:
|
||||
setRotation(*(*uint)(data) % 4)
|
||||
return true
|
||||
case C.RETRO_ENVIRONMENT_GET_VARIABLE:
|
||||
variable := (*C.struct_retro_variable)(data)
|
||||
key := C.GoString(variable.key)
|
||||
if val, ok := coreConfig[key]; ok {
|
||||
log.Printf("[Env]: get variable: key:%v value:%v", key, C.GoString(val))
|
||||
variable.value = val
|
||||
return true
|
||||
}
|
||||
// fmt.Printf("[Env]: get variable: key:%v not found\n", key)
|
||||
return false
|
||||
case C.RETRO_ENVIRONMENT_SET_HW_RENDER:
|
||||
video.isGl = isGlAllowed
|
||||
if isGlAllowed {
|
||||
video.hw = (*C.struct_retro_hw_render_callback)(data)
|
||||
video.hw.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
|
||||
video.hw.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
case C.RETRO_ENVIRONMENT_SET_CONTROLLER_INFO:
|
||||
if multitap.supported {
|
||||
info := (*[100]C.struct_retro_controller_info)(data)
|
||||
var i C.unsigned
|
||||
for i = 0; unsafe.Pointer(info[i].types) != nil; i++ {
|
||||
var j C.unsigned
|
||||
types := (*[100]C.struct_retro_controller_description)(unsafe.Pointer(info[i].types))
|
||||
for j = 0; j < info[i].num_types; j++ {
|
||||
if C.GoString(types[j].desc) == "Multitap" {
|
||||
multitap.value = types[j].id
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
default:
|
||||
//fmt.Println("[Env]: command not implemented", cmd)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//export initVideo
|
||||
func initVideo() {
|
||||
var context graphics.Context
|
||||
switch video.hw.context_type {
|
||||
case C.RETRO_HW_CONTEXT_NONE:
|
||||
context = graphics.CtxNone
|
||||
case C.RETRO_HW_CONTEXT_OPENGL:
|
||||
context = graphics.CtxOpenGl
|
||||
case C.RETRO_HW_CONTEXT_OPENGLES2:
|
||||
context = graphics.CtxOpenGlEs2
|
||||
case C.RETRO_HW_CONTEXT_OPENGL_CORE:
|
||||
context = graphics.CtxOpenGlCore
|
||||
case C.RETRO_HW_CONTEXT_OPENGLES3:
|
||||
context = graphics.CtxOpenGlEs3
|
||||
case C.RETRO_HW_CONTEXT_OPENGLES_VERSION:
|
||||
context = graphics.CtxOpenGlEsVersion
|
||||
case C.RETRO_HW_CONTEXT_VULKAN:
|
||||
context = graphics.CtxVulkan
|
||||
case C.RETRO_HW_CONTEXT_DUMMY:
|
||||
context = graphics.CtxDummy
|
||||
default:
|
||||
context = graphics.CtxUnknown
|
||||
}
|
||||
|
||||
graphics.Init(graphics.Config{
|
||||
Ctx: context,
|
||||
W: int(video.maxWidth),
|
||||
H: int(video.maxHeight),
|
||||
Gl: graphics.GlConfig{
|
||||
AutoContext: video.autoGlContext,
|
||||
VersionMajor: uint(video.hw.version_major),
|
||||
VersionMinor: uint(video.hw.version_minor),
|
||||
HasDepth: bool(video.hw.depth),
|
||||
HasStencil: bool(video.hw.stencil),
|
||||
},
|
||||
})
|
||||
C.bridge_context_reset(video.hw.context_reset)
|
||||
}
|
||||
|
||||
//export deinitVideo
|
||||
func deinitVideo() {
|
||||
C.bridge_context_reset(video.hw.context_destroy)
|
||||
graphics.Deinit()
|
||||
video.isGl = false
|
||||
video.autoGlContext = false
|
||||
}
|
||||
|
||||
var (
|
||||
retroAPIVersion unsafe.Pointer
|
||||
retroDeinit unsafe.Pointer
|
||||
retroGetSystemAVInfo unsafe.Pointer
|
||||
retroGetSystemInfo unsafe.Pointer
|
||||
retroHandle unsafe.Pointer
|
||||
retroInit unsafe.Pointer
|
||||
retroLoadGame unsafe.Pointer
|
||||
retroRun unsafe.Pointer
|
||||
retroSetAudioSample unsafe.Pointer
|
||||
retroSetAudioSampleBatch unsafe.Pointer
|
||||
retroSetControllerPortDevice unsafe.Pointer
|
||||
retroSetEnvironment unsafe.Pointer
|
||||
retroSetInputPoll unsafe.Pointer
|
||||
retroSetInputState unsafe.Pointer
|
||||
retroSetVideoRefresh unsafe.Pointer
|
||||
retroUnloadGame unsafe.Pointer
|
||||
)
|
||||
|
||||
func coreLoad(meta emulator.Metadata) {
|
||||
isGlAllowed = meta.IsGlAllowed
|
||||
usesLibCo = meta.UsesLibCo
|
||||
video.autoGlContext = meta.AutoGlContext
|
||||
coreConfig = ScanConfigFile(meta.ConfigPath)
|
||||
|
||||
multitap.supported = meta.HasMultitap
|
||||
multitap.enabled = false
|
||||
multitap.value = 0
|
||||
|
||||
filePath := meta.LibPath
|
||||
if arch, err := core.GetCoreExt(); err == nil {
|
||||
filePath = filePath + arch.LibExt
|
||||
} else {
|
||||
log.Printf("warning: %v", err)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
var err error
|
||||
retroHandle, err = loadLib(filePath)
|
||||
// fallback to sequential lib loader (first successfully loaded)
|
||||
if err != nil {
|
||||
retroHandle, err = loadLibRollingRollingRolling(filePath)
|
||||
if err != nil {
|
||||
log.Fatalf("error core load: %s, %v", filePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
retroInit = loadFunction(retroHandle, "retro_init")
|
||||
retroDeinit = loadFunction(retroHandle, "retro_deinit")
|
||||
retroAPIVersion = loadFunction(retroHandle, "retro_api_version")
|
||||
retroGetSystemInfo = loadFunction(retroHandle, "retro_get_system_info")
|
||||
retroGetSystemAVInfo = loadFunction(retroHandle, "retro_get_system_av_info")
|
||||
retroSetEnvironment = loadFunction(retroHandle, "retro_set_environment")
|
||||
retroSetVideoRefresh = loadFunction(retroHandle, "retro_set_video_refresh")
|
||||
retroSetInputPoll = loadFunction(retroHandle, "retro_set_input_poll")
|
||||
retroSetInputState = loadFunction(retroHandle, "retro_set_input_state")
|
||||
retroSetAudioSample = loadFunction(retroHandle, "retro_set_audio_sample")
|
||||
retroSetAudioSampleBatch = loadFunction(retroHandle, "retro_set_audio_sample_batch")
|
||||
retroRun = loadFunction(retroHandle, "retro_run")
|
||||
retroLoadGame = loadFunction(retroHandle, "retro_load_game")
|
||||
retroUnloadGame = loadFunction(retroHandle, "retro_unload_game")
|
||||
retroSerializeSize = loadFunction(retroHandle, "retro_serialize_size")
|
||||
retroSerialize = loadFunction(retroHandle, "retro_serialize")
|
||||
retroUnserialize = loadFunction(retroHandle, "retro_unserialize")
|
||||
retroSetControllerPortDevice = loadFunction(retroHandle, "retro_set_controller_port_device")
|
||||
retroGetMemorySize = loadFunction(retroHandle, "retro_get_memory_size")
|
||||
retroGetMemoryData = loadFunction(retroHandle, "retro_get_memory_data")
|
||||
|
||||
mu.Unlock()
|
||||
|
||||
C.bridge_retro_set_environment(retroSetEnvironment, C.coreEnvironment_cgo)
|
||||
C.bridge_retro_set_video_refresh(retroSetVideoRefresh, C.coreVideoRefresh_cgo)
|
||||
C.bridge_retro_set_input_poll(retroSetInputPoll, C.coreInputPoll_cgo)
|
||||
C.bridge_retro_set_input_state(retroSetInputState, C.coreInputState_cgo)
|
||||
C.bridge_retro_set_audio_sample(retroSetAudioSample, C.coreAudioSample_cgo)
|
||||
C.bridge_retro_set_audio_sample_batch(retroSetAudioSampleBatch, C.coreAudioSampleBatch_cgo)
|
||||
|
||||
C.bridge_retro_init(retroInit)
|
||||
|
||||
v := C.bridge_retro_api_version(retroAPIVersion)
|
||||
log.Printf("Libretro API version: %v", v)
|
||||
}
|
||||
|
||||
func slurp(path string, size int64) ([]byte, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
bytes := make([]byte, size)
|
||||
buffer := bufio.NewReader(f)
|
||||
_, err = buffer.Read(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bytes, nil
|
||||
}
|
||||
|
||||
func coreLoadGame(filename string) {
|
||||
lastFrameTime = 0
|
||||
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = file.Close()
|
||||
|
||||
size := fi.Size()
|
||||
log.Printf("ROM size: %v", size)
|
||||
|
||||
csFilename := C.CString(filename)
|
||||
defer C.free(unsafe.Pointer(csFilename))
|
||||
gi := C.struct_retro_game_info{
|
||||
path: csFilename,
|
||||
size: C.size_t(size),
|
||||
}
|
||||
|
||||
si := C.struct_retro_system_info{}
|
||||
C.bridge_retro_get_system_info(retroGetSystemInfo, &si)
|
||||
log.Printf(" library_name: %v", C.GoString(si.library_name))
|
||||
log.Printf(" library_version: %v", C.GoString(si.library_version))
|
||||
log.Printf(" valid_extensions: %v", C.GoString(si.valid_extensions))
|
||||
log.Printf(" need_fullpath: %v", bool(si.need_fullpath))
|
||||
log.Printf(" block_extract: %v", bool(si.block_extract))
|
||||
|
||||
if !si.need_fullpath {
|
||||
bytes, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cstr := C.CString(string(bytes))
|
||||
defer C.free(unsafe.Pointer(cstr))
|
||||
gi.data = unsafe.Pointer(cstr)
|
||||
}
|
||||
|
||||
ok := C.bridge_retro_load_game(retroLoadGame, &gi)
|
||||
if !ok {
|
||||
log.Fatal("The core failed to load the content.")
|
||||
}
|
||||
|
||||
avi := C.struct_retro_system_av_info{}
|
||||
C.bridge_retro_get_system_av_info(retroGetSystemAVInfo, &avi)
|
||||
|
||||
// Append the library name to the window title.
|
||||
NAEmulator.meta.AudioSampleRate = int(avi.timing.sample_rate)
|
||||
NAEmulator.meta.Fps = float64(avi.timing.fps)
|
||||
NAEmulator.meta.BaseWidth = int(avi.geometry.base_width)
|
||||
NAEmulator.meta.BaseHeight = int(avi.geometry.base_height)
|
||||
// set aspect ratio
|
||||
/* Nominal aspect ratio of game. If aspect_ratio is <= 0.0,
|
||||
an aspect ratio of base_width / base_height is assumed.
|
||||
* A frontend could override this setting, if desired. */
|
||||
ratio := float64(avi.geometry.aspect_ratio)
|
||||
if ratio <= 0.0 {
|
||||
ratio = float64(avi.geometry.base_width) / float64(avi.geometry.base_height)
|
||||
}
|
||||
NAEmulator.meta.Ratio = ratio
|
||||
|
||||
log.Printf("-----------------------------------")
|
||||
log.Printf("--- Core audio and video info ---")
|
||||
log.Printf("-----------------------------------")
|
||||
log.Printf(" Frame: %vx%v (%vx%v)",
|
||||
avi.geometry.base_width, avi.geometry.base_height,
|
||||
avi.geometry.max_width, avi.geometry.max_height)
|
||||
log.Printf(" AR: %v", ratio)
|
||||
log.Printf(" FPS: %v", avi.timing.fps)
|
||||
log.Printf(" Audio: %vHz", avi.timing.sample_rate)
|
||||
log.Printf("-----------------------------------")
|
||||
|
||||
video.maxWidth = int32(avi.geometry.max_width)
|
||||
video.maxHeight = int32(avi.geometry.max_height)
|
||||
video.baseWidth = int32(avi.geometry.base_width)
|
||||
video.baseHeight = int32(avi.geometry.base_height)
|
||||
if video.isGl {
|
||||
bufS := int(video.maxWidth * video.maxHeight * int32(video.bpp))
|
||||
graphics.SetBuffer(bufS)
|
||||
log.Printf("Set buffer: %v", byteCountBinary(int64(bufS)))
|
||||
if usesLibCo {
|
||||
C.bridge_execute(C.initVideo_cgo)
|
||||
} else {
|
||||
runtime.LockOSThread()
|
||||
initVideo()
|
||||
runtime.UnlockOSThread()
|
||||
}
|
||||
}
|
||||
|
||||
// set default controller types on all ports
|
||||
maxPort := 4 // controllersNum
|
||||
for i := 0; i < maxPort; i++ {
|
||||
C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, C.uint(i), C.RETRO_DEVICE_JOYPAD)
|
||||
}
|
||||
}
|
||||
|
||||
func toggleMultitap() {
|
||||
if multitap.supported && multitap.value != 0 {
|
||||
// Official SNES games only support a single multitap device
|
||||
// Most require it to be plugged in player 2 port
|
||||
// And Snes9X requires it to be "plugged" after the game is loaded
|
||||
// Control this from the browser since player 2 will stop working in some games if multitap is "plugged" in
|
||||
if multitap.enabled {
|
||||
C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, C.RETRO_DEVICE_JOYPAD)
|
||||
} else {
|
||||
C.bridge_retro_set_controller_port_device(retroSetControllerPortDevice, 1, multitap.value)
|
||||
}
|
||||
multitap.enabled = !multitap.enabled
|
||||
}
|
||||
}
|
||||
|
||||
func nanoarchShutdown() {
|
||||
if usesLibCo {
|
||||
thread.Main(func() {
|
||||
C.bridge_execute(retroUnloadGame)
|
||||
C.bridge_execute(retroDeinit)
|
||||
if video.isGl {
|
||||
C.bridge_execute(C.deinitVideo_cgo)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if video.isGl {
|
||||
thread.Main(func() {
|
||||
// running inside a go routine, lock the thread to make sure the OpenGL context stays current
|
||||
runtime.LockOSThread()
|
||||
graphics.BindContext()
|
||||
})
|
||||
}
|
||||
C.bridge_retro_unload_game(retroUnloadGame)
|
||||
C.bridge_retro_deinit(retroDeinit)
|
||||
if video.isGl {
|
||||
thread.Main(func() {
|
||||
deinitVideo()
|
||||
runtime.UnlockOSThread()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
setRotation(0)
|
||||
if err := closeLib(retroHandle); err != nil {
|
||||
log.Printf("error when close: %v", err)
|
||||
}
|
||||
for _, element := range coreConfig {
|
||||
C.free(unsafe.Pointer(element))
|
||||
}
|
||||
image.Clear()
|
||||
}
|
||||
|
||||
func nanoarchRun() {
|
||||
if usesLibCo {
|
||||
C.bridge_execute(retroRun)
|
||||
} else {
|
||||
if video.isGl {
|
||||
// running inside a go routine, lock the thread to make sure the OpenGL context stays current
|
||||
runtime.LockOSThread()
|
||||
graphics.BindContext()
|
||||
}
|
||||
C.bridge_retro_run(retroRun)
|
||||
if video.isGl {
|
||||
runtime.UnlockOSThread()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func videoSetPixelFormat(format uint32) C.bool {
|
||||
switch format {
|
||||
case C.RETRO_PIXEL_FORMAT_0RGB1555:
|
||||
video.pixFmt = image.BitFormatShort5551
|
||||
graphics.SetPixelFormat(graphics.UnsignedShort5551)
|
||||
video.bpp = 2
|
||||
// format is not implemented
|
||||
pixelFormatConverterFn = nil
|
||||
case C.RETRO_PIXEL_FORMAT_XRGB8888:
|
||||
video.pixFmt = image.BitFormatInt8888Rev
|
||||
graphics.SetPixelFormat(graphics.UnsignedInt8888Rev)
|
||||
video.bpp = 4
|
||||
pixelFormatConverterFn = image.Rgba8888
|
||||
case C.RETRO_PIXEL_FORMAT_RGB565:
|
||||
video.pixFmt = image.BitFormatShort565
|
||||
graphics.SetPixelFormat(graphics.UnsignedShort565)
|
||||
video.bpp = 2
|
||||
pixelFormatConverterFn = image.Rgb565
|
||||
default:
|
||||
log.Fatalf("Unknown pixel type %v", format)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setRotation(rotation uint) {
|
||||
if rotation == uint(video.rotation) {
|
||||
return
|
||||
}
|
||||
video.rotation = image.Angle(rotation)
|
||||
r := image.GetRotation(video.rotation)
|
||||
if rotation > 0 {
|
||||
rotationFn = &r
|
||||
} else {
|
||||
rotationFn = nil
|
||||
}
|
||||
NAEmulator.meta.Rotation = r
|
||||
log.Printf("[Env]: the game video is rotated %v°", map[uint]uint{0: 0, 1: 90, 2: 180, 3: 270}[rotation])
|
||||
}
|
||||
|
||||
func byteCountBinary(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
// Save writes the current state to the filesystem.
|
||||
func (na *naEmulator) Save() error {
|
||||
na.Lock()
|
||||
defer na.Unlock()
|
||||
|
||||
ss, err := getSaveState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := na.storage.Save(na.GetHashPath(), ss); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if sram := getSaveRAM(); sram != nil {
|
||||
if err := na.storage.Save(na.GetSRAMPath(), sram); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load restores the state from the filesystem.
|
||||
func (na *naEmulator) Load() error {
|
||||
na.Lock()
|
||||
defer na.Unlock()
|
||||
|
||||
ss, err := na.storage.Load(na.GetHashPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := restoreSaveState(ss); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sram, err := na.storage.Load(na.GetSRAMPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sram != nil {
|
||||
restoreSaveRAM(sram)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
/*
|
||||
#include "libretro.h"
|
||||
#include <stdlib.h>
|
||||
|
||||
size_t bridge_retro_get_memory_size(void *f, unsigned id);
|
||||
void* bridge_retro_get_memory_data(void *f, unsigned id);
|
||||
bool bridge_retro_serialize(void *f, void *data, size_t size);
|
||||
bool bridge_retro_unserialize(void *f, void *data, size_t size);
|
||||
size_t bridge_retro_serialize_size(void *f);
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// !global emulator lib state
|
||||
var (
|
||||
retroGetMemoryData unsafe.Pointer
|
||||
retroGetMemorySize unsafe.Pointer
|
||||
retroSerialize unsafe.Pointer
|
||||
retroSerializeSize unsafe.Pointer
|
||||
retroUnserialize unsafe.Pointer
|
||||
)
|
||||
|
||||
// defines any memory state of the emulator
|
||||
type state []byte
|
||||
|
||||
type mem struct {
|
||||
ptr unsafe.Pointer
|
||||
size uint
|
||||
}
|
||||
|
||||
// saveStateSize returns the amount of data the implementation requires
|
||||
// to serialize internal state (save states).
|
||||
func saveStateSize() uint { return uint(C.bridge_retro_serialize_size(retroSerializeSize)) }
|
||||
|
||||
// getSaveState returns emulator internal state.
|
||||
func getSaveState() (state, error) {
|
||||
size := saveStateSize()
|
||||
data := C.malloc(C.size_t(size))
|
||||
defer C.free(data)
|
||||
if !bool(C.bridge_retro_serialize(retroSerialize, data, C.size_t(size))) {
|
||||
return nil, errors.New("retro_serialize failed")
|
||||
}
|
||||
return C.GoBytes(data, C.int(size)), nil
|
||||
}
|
||||
|
||||
// restoreSaveState restores emulator internal state.
|
||||
func restoreSaveState(st state) error {
|
||||
if len(st) == 0 {
|
||||
return nil
|
||||
}
|
||||
size := saveStateSize()
|
||||
if !bool(C.bridge_retro_unserialize(retroUnserialize, unsafe.Pointer(&st[0]), C.size_t(size))) {
|
||||
return errors.New("retro_unserialize failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSaveRAM returns the game save RAM (cartridge) data or a nil slice.
|
||||
func getSaveRAM() state {
|
||||
mem := ptSaveRAM()
|
||||
if mem == nil {
|
||||
return nil
|
||||
}
|
||||
return C.GoBytes(mem.ptr, C.int(mem.size))
|
||||
}
|
||||
|
||||
// restoreSaveRAM restores game save RAM.
|
||||
func restoreSaveRAM(st state) {
|
||||
if len(st) == 0 {
|
||||
return
|
||||
}
|
||||
if mem := ptSaveRAM(); mem != nil {
|
||||
sram := (*[1 << 30]byte)(mem.ptr)[:mem.size:mem.size]
|
||||
copy(sram, st)
|
||||
}
|
||||
}
|
||||
|
||||
// getMemorySize returns memory region size.
|
||||
func getMemorySize(id uint) uint {
|
||||
return uint(C.bridge_retro_get_memory_size(retroGetMemorySize, C.uint(id)))
|
||||
}
|
||||
|
||||
// getMemoryData returns a pointer to memory data.
|
||||
func getMemoryData(id uint) unsafe.Pointer {
|
||||
return C.bridge_retro_get_memory_data(retroGetMemoryData, C.uint(id))
|
||||
}
|
||||
|
||||
// ptSaveRam return SRAM memory pointer if core supports it or nil.
|
||||
func ptSaveRAM() *mem {
|
||||
ptr, size := getMemoryData(C.RETRO_MEMORY_SAVE_RAM), getMemorySize(C.RETRO_MEMORY_SAVE_RAM)
|
||||
if ptr == nil || size == 0 {
|
||||
return nil
|
||||
}
|
||||
return &mem{ptr: ptr, size: size}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type (
|
||||
Storage interface {
|
||||
GetSavePath() string
|
||||
GetSRAMPath() string
|
||||
Load(path string) ([]byte, error)
|
||||
Save(path string, data []byte) error
|
||||
}
|
||||
StateStorage struct {
|
||||
// save path without the dir slash in the end
|
||||
Path string
|
||||
// contains the name of the main save file
|
||||
// e.g. abc<...>293.dat
|
||||
// needed for Google Cloud save/restore which
|
||||
// doesn't support multiple files
|
||||
MainSave string
|
||||
}
|
||||
)
|
||||
|
||||
func (s *StateStorage) GetSavePath() string { return filepath.Join(s.Path, s.MainSave+".dat") }
|
||||
func (s *StateStorage) GetSRAMPath() string { return filepath.Join(s.Path, s.MainSave+".srm") }
|
||||
func (s *StateStorage) Load(path string) ([]byte, error) { return ioutil.ReadFile(path) }
|
||||
func (s *StateStorage) Save(path string, dat []byte) error { return ioutil.WriteFile(path, dat, 0644) }
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/compression/zip"
|
||||
)
|
||||
|
||||
type ZipStorage struct {
|
||||
Storage
|
||||
}
|
||||
|
||||
func (z *ZipStorage) GetSavePath() string { return z.Storage.GetSavePath() + zip.Ext }
|
||||
func (z *ZipStorage) GetSRAMPath() string { return z.Storage.GetSRAMPath() + zip.Ext }
|
||||
|
||||
// Load loads a zip file with the path specified.
|
||||
func (z *ZipStorage) Load(path string) ([]byte, error) {
|
||||
data, err := z.Storage.Load(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d, _, err := zip.Read(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// Save saves the array of bytes into a file with the specified path.
|
||||
func (z *ZipStorage) Save(path string, data []byte) error {
|
||||
_, name := filepath.Split(path)
|
||||
if name == "" || name == "." {
|
||||
return zip.ErrorInvalidName
|
||||
}
|
||||
name = strings.TrimSuffix(name, zip.Ext)
|
||||
compress, err := zip.Compress(data, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return z.Storage.Save(path, compress)
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
package h264
|
||||
|
||||
type Options struct {
|
||||
// Constant Rate Factor (CRF)
|
||||
// This method allows the encoder to attempt to achieve a certain output quality for the whole file
|
||||
// when output file size is of less importance.
|
||||
// The range of the CRF scale is 0–51, where 0 is lossless, 23 is the default, and 51 is worst quality possible.
|
||||
Crf uint8
|
||||
// film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency.
|
||||
Tune string
|
||||
// ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo.
|
||||
Preset string
|
||||
// baseline, main, high, high10, high422, high444.
|
||||
Profile string
|
||||
LogLevel int32
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func WithOptions(arg Options) Option {
|
||||
return func(args *Options) {
|
||||
args.Crf = arg.Crf
|
||||
args.Tune = arg.Tune
|
||||
args.Preset = arg.Preset
|
||||
args.Profile = arg.Profile
|
||||
args.LogLevel = arg.LogLevel
|
||||
}
|
||||
}
|
||||
func Crf(arg uint8) Option { return func(args *Options) { args.Crf = arg } }
|
||||
func Tune(arg string) Option { return func(args *Options) { args.Tune = arg } }
|
||||
func Preset(arg string) Option { return func(args *Options) { args.Preset = arg } }
|
||||
func Profile(arg string) Option { return func(args *Options) { args.Profile = arg } }
|
||||
func LogLevel(arg int32) Option { return func(args *Options) { args.LogLevel = arg } }
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
package h264
|
||||
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
type H264 struct {
|
||||
ref *T
|
||||
|
||||
width int32
|
||||
lumaSize int32
|
||||
chromaSize int32
|
||||
csp int32
|
||||
nnals int32
|
||||
nals []*Nal
|
||||
|
||||
// keep monotonic pts to suppress warnings
|
||||
pts int64
|
||||
}
|
||||
|
||||
func NewEncoder(width, height int, options ...Option) (encoder *H264, err error) {
|
||||
libVersion := int(Build)
|
||||
|
||||
if libVersion < 150 {
|
||||
return nil, fmt.Errorf("x264: the library version should be newer than v150, you have got version %v", libVersion)
|
||||
}
|
||||
|
||||
if libVersion < 160 {
|
||||
log.Printf("x264: warning, installed version of libx264 %v is older than minimally supported v160, expect bugs", libVersion)
|
||||
}
|
||||
|
||||
opts := &Options{
|
||||
Crf: 12,
|
||||
Tune: "zerolatency",
|
||||
Preset: "superfast",
|
||||
Profile: "baseline",
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(opts)
|
||||
}
|
||||
|
||||
if opts.LogLevel > 0 {
|
||||
log.Printf("x264: build v%v", Build)
|
||||
}
|
||||
|
||||
param := Param{}
|
||||
if opts.Preset != "" && opts.Tune != "" {
|
||||
if ParamDefaultPreset(¶m, opts.Preset, opts.Tune) < 0 {
|
||||
return nil, fmt.Errorf("x264: invalid preset/tune name")
|
||||
}
|
||||
} else {
|
||||
ParamDefault(¶m)
|
||||
}
|
||||
|
||||
if opts.Profile != "" {
|
||||
if ParamApplyProfile(¶m, opts.Profile) < 0 {
|
||||
return nil, fmt.Errorf("x264: invalid profile name")
|
||||
}
|
||||
}
|
||||
|
||||
// legacy encoder lacks of this param
|
||||
param.IBitdepth = 8
|
||||
|
||||
if libVersion > 155 {
|
||||
param.ICsp = CspI420
|
||||
} else {
|
||||
param.ICsp = 1
|
||||
}
|
||||
param.IWidth = int32(width)
|
||||
param.IHeight = int32(height)
|
||||
param.ILogLevel = opts.LogLevel
|
||||
|
||||
param.Rc.IRcMethod = RcCrf
|
||||
param.Rc.FRfConstant = float32(opts.Crf)
|
||||
|
||||
encoder = &H264{
|
||||
csp: param.ICsp,
|
||||
lumaSize: int32(width * height),
|
||||
chromaSize: int32(width*height) / 4,
|
||||
nals: make([]*Nal, 1),
|
||||
width: int32(width),
|
||||
}
|
||||
|
||||
if encoder.ref = EncoderOpen(¶m); encoder.ref == nil {
|
||||
err = fmt.Errorf("x264: cannot open the encoder")
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (e *H264) Encode(yuv []byte) []byte {
|
||||
var picIn, picOut Picture
|
||||
|
||||
picIn.Img.ICsp = e.csp
|
||||
picIn.Img.IPlane = 3
|
||||
picIn.Img.IStride[0] = e.width
|
||||
picIn.Img.IStride[1] = e.width / 2
|
||||
picIn.Img.IStride[2] = e.width / 2
|
||||
|
||||
picIn.Img.Plane[0] = C.CBytes(yuv[:e.lumaSize])
|
||||
picIn.Img.Plane[1] = C.CBytes(yuv[e.lumaSize : e.lumaSize+e.chromaSize])
|
||||
picIn.Img.Plane[2] = C.CBytes(yuv[e.lumaSize+e.chromaSize:])
|
||||
|
||||
picIn.IPts = e.pts
|
||||
e.pts++
|
||||
|
||||
defer func() {
|
||||
picIn.freePlane(0)
|
||||
picIn.freePlane(1)
|
||||
picIn.freePlane(2)
|
||||
}()
|
||||
|
||||
if ret := EncoderEncode(e.ref, e.nals, &e.nnals, &picIn, &picOut); ret > 0 {
|
||||
return C.GoBytes(e.nals[0].PPayload, C.int(ret))
|
||||
// ret should be equal to writer writes
|
||||
}
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
func (e *H264) Shutdown() error {
|
||||
EncoderClose(e.ref)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
package opus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
type Encoder struct {
|
||||
*LibOpusEncoder
|
||||
|
||||
buf []byte
|
||||
}
|
||||
|
||||
func NewEncoder(outFq, channels int, options ...func(*Encoder) error) (*Encoder, error) {
|
||||
encoder, err := NewOpusEncoder(outFq, channels, AppRestrictedLowdelay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
enc := &Encoder{LibOpusEncoder: encoder, buf: make([]byte, 1000)}
|
||||
var result *multierror.Error
|
||||
result = multierror.Append(result,
|
||||
enc.SetMaxBandwidth(FullBand),
|
||||
enc.SetBitrate(192000),
|
||||
enc.SetComplexity(10),
|
||||
)
|
||||
for _, option := range options {
|
||||
result = multierror.Append(option(enc))
|
||||
}
|
||||
return enc, result.ErrorOrNil()
|
||||
}
|
||||
|
||||
func (e *Encoder) Encode(pcm []int16) ([]byte, error) {
|
||||
n, err := e.LibOpusEncoder.Encode(pcm, e.buf)
|
||||
// n = 1 is DTX
|
||||
if err != nil || n == 1 {
|
||||
return []byte{}, err
|
||||
}
|
||||
return e.buf[:n], nil
|
||||
}
|
||||
|
||||
func (e *Encoder) GetInfo() string {
|
||||
bitrate, _ := e.LibOpusEncoder.Bitrate()
|
||||
complexity, _ := e.LibOpusEncoder.Complexity()
|
||||
dtx, _ := e.LibOpusEncoder.DTX()
|
||||
fec, _ := e.LibOpusEncoder.FEC()
|
||||
maxBandwidth, _ := e.LibOpusEncoder.MaxBandwidth()
|
||||
lossPercent, _ := e.LibOpusEncoder.PacketLossPerc()
|
||||
sampleRate, _ := e.LibOpusEncoder.SampleRate()
|
||||
return fmt.Sprintf(
|
||||
"%v, Bitrate: %v bps, Complexity: %v, DTX: %v, FEC: %v, Max bandwidth: *%v, Loss%%: %v, Rate: %v Hz",
|
||||
CodecVersion(), bitrate, complexity, dtx, fec, maxBandwidth, lossPercent, sampleRate,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,200 +0,0 @@
|
|||
package opus
|
||||
|
||||
/*
|
||||
#cgo pkg-config: opus
|
||||
#cgo CFLAGS: -Wall -O3
|
||||
|
||||
#include <opus.h>
|
||||
|
||||
int bridge_encoder_get_bitrate(OpusEncoder *st, opus_int32 *bitrate) { return opus_encoder_ctl(st, OPUS_GET_BITRATE(bitrate)); }
|
||||
int bridge_encoder_get_complexity(OpusEncoder *st, opus_int32 *complexity) { return opus_encoder_ctl(st, OPUS_GET_COMPLEXITY(complexity)); }
|
||||
int bridge_encoder_get_dtx(OpusEncoder *st, opus_int32 *dtx) { return opus_encoder_ctl(st, OPUS_GET_DTX(dtx)); }
|
||||
int bridge_encoder_get_inband_fec(OpusEncoder *st, opus_int32 *fec) { return opus_encoder_ctl(st, OPUS_GET_INBAND_FEC(fec)); }
|
||||
int bridge_encoder_get_max_bandwidth(OpusEncoder *st, opus_int32 *max_bw) { return opus_encoder_ctl(st, OPUS_GET_MAX_BANDWIDTH(max_bw)); }
|
||||
int bridge_encoder_get_packet_loss_perc(OpusEncoder *st, opus_int32 *loss_perc) { return opus_encoder_ctl(st, OPUS_GET_PACKET_LOSS_PERC(loss_perc)); }
|
||||
int bridge_encoder_get_sample_rate(OpusEncoder *st, opus_int32 *sample_rate) { return opus_encoder_ctl(st, OPUS_GET_SAMPLE_RATE(sample_rate)); }
|
||||
int bridge_encoder_set_bitrate(OpusEncoder *st, opus_int32 bitrate) { return opus_encoder_ctl(st, OPUS_SET_BITRATE(bitrate)); }
|
||||
int bridge_encoder_set_complexity(OpusEncoder *st, opus_int32 complexity) { return opus_encoder_ctl(st, OPUS_SET_COMPLEXITY(complexity)); }
|
||||
int bridge_encoder_set_dtx(OpusEncoder *st, opus_int32 use_dtx) { return opus_encoder_ctl(st, OPUS_SET_DTX(use_dtx)); }
|
||||
int bridge_encoder_set_inband_fec(OpusEncoder *st, opus_int32 fec) { return opus_encoder_ctl(st, OPUS_SET_INBAND_FEC(fec)); }
|
||||
int bridge_encoder_set_max_bandwidth(OpusEncoder *st, opus_int32 max_bw) { return opus_encoder_ctl(st, OPUS_SET_MAX_BANDWIDTH(max_bw)); }
|
||||
int bridge_encoder_set_packet_loss_perc(OpusEncoder *st, opus_int32 loss_perc) { return opus_encoder_ctl(st, OPUS_SET_PACKET_LOSS_PERC(loss_perc)); }
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type (
|
||||
Application int
|
||||
Bandwidth int
|
||||
Bitrate int
|
||||
Error int
|
||||
)
|
||||
|
||||
const (
|
||||
// Optimize encoding for VoIP
|
||||
//AppVoIP = Application(C.OPUS_APPLICATION_VOIP)
|
||||
// Optimize encoding for non-voice signals like music
|
||||
//AppAudio = Application(C.OPUS_APPLICATION_AUDIO)
|
||||
// Optimize encoding for low latency applications
|
||||
AppRestrictedLowdelay = Application(C.OPUS_APPLICATION_RESTRICTED_LOWDELAY)
|
||||
|
||||
// Auto/default setting
|
||||
//BitrateAuto = Bitrate(-1000)
|
||||
//BitrateMax = Bitrate(-1)
|
||||
|
||||
// 20 kHz bandpass
|
||||
FullBand = Bandwidth(C.OPUS_BANDWIDTH_FULLBAND)
|
||||
)
|
||||
|
||||
//const (
|
||||
// ErrorOK = Error(C.OPUS_OK)
|
||||
// ErrorBadArg = Error(C.OPUS_BAD_ARG)
|
||||
// ErrorBufferTooSmall = Error(C.OPUS_BUFFER_TOO_SMALL)
|
||||
// ErrorInternalError = Error(C.OPUS_INTERNAL_ERROR)
|
||||
// ErrorInvalidPacket = Error(C.OPUS_INVALID_PACKET)
|
||||
// ErrorUnimplemented = Error(C.OPUS_UNIMPLEMENTED)
|
||||
// ErrorInvalidState = Error(C.OPUS_INVALID_STATE)
|
||||
// ErrorAllocFail = Error(C.OPUS_ALLOC_FAIL)
|
||||
//)
|
||||
|
||||
type LibOpusEncoder struct {
|
||||
buf []byte
|
||||
channels int
|
||||
ptr *C.struct_OpusEncoder
|
||||
}
|
||||
|
||||
// NewOpusEncoder creates new Opus encoder.
|
||||
func NewOpusEncoder(sampleRate int, channels int, app Application) (*LibOpusEncoder, error) {
|
||||
var enc LibOpusEncoder
|
||||
if enc.ptr != nil {
|
||||
return nil, fmt.Errorf("opus: encoder reinit")
|
||||
}
|
||||
enc.channels = channels
|
||||
// !to check mem leak
|
||||
enc.buf = make([]byte, C.opus_encoder_get_size(C.int(channels)))
|
||||
enc.ptr = (*C.OpusEncoder)(unsafe.Pointer(&enc.buf[0]))
|
||||
err := unwrap(C.opus_encoder_init(enc.ptr, C.opus_int32(sampleRate), C.int(channels), C.int(app)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opus: initializatoin error (%v)", err)
|
||||
}
|
||||
return &enc, nil
|
||||
}
|
||||
|
||||
// Encode converts raw PCM samples into the supplied Opus buffer.
|
||||
// Returns the number of bytes converted.
|
||||
func (enc *LibOpusEncoder) Encode(pcm []int16, data []byte) (rez int, err error) {
|
||||
if len(pcm) == 0 {
|
||||
return
|
||||
}
|
||||
samples := C.int(len(pcm) / enc.channels)
|
||||
n := C.opus_encode(enc.ptr, (*C.opus_int16)(&pcm[0]), samples, (*C.uchar)(&data[0]), C.opus_int32(cap(data)))
|
||||
if n > 0 {
|
||||
rez = int(n)
|
||||
}
|
||||
return rez, unwrap(n)
|
||||
}
|
||||
|
||||
// SampleRate returns the sample rate of the encoder.
|
||||
func (enc *LibOpusEncoder) SampleRate() (int, error) {
|
||||
var sampleRate C.opus_int32
|
||||
res := C.bridge_encoder_get_sample_rate(enc.ptr, &sampleRate)
|
||||
return int(sampleRate), unwrap(res)
|
||||
}
|
||||
|
||||
// Bitrate returns the bitrate of the encoder.
|
||||
func (enc *LibOpusEncoder) Bitrate() (int, error) {
|
||||
var bitrate C.opus_int32
|
||||
res := C.bridge_encoder_get_bitrate(enc.ptr, &bitrate)
|
||||
return int(bitrate), unwrap(res)
|
||||
}
|
||||
|
||||
// SetBitrate sets the bitrate of the encoder.
|
||||
// BitrateMax / BitrateAuto can be used here.
|
||||
func (enc *LibOpusEncoder) SetBitrate(b Bitrate) error {
|
||||
return unwrap(C.bridge_encoder_set_bitrate(enc.ptr, C.opus_int32(b)))
|
||||
}
|
||||
|
||||
// Complexity returns the value of the complexity.
|
||||
func (enc *LibOpusEncoder) Complexity() (int, error) {
|
||||
var complexity C.opus_int32
|
||||
res := C.bridge_encoder_get_complexity(enc.ptr, &complexity)
|
||||
return int(complexity), unwrap(res)
|
||||
}
|
||||
|
||||
// SetComplexity sets the complexity factor for the encoder.
|
||||
// Complexity is a value from 1 to 10, where 1 is the lowest complexity and 10 is the highest.
|
||||
func (enc *LibOpusEncoder) SetComplexity(complexity int) error {
|
||||
return unwrap(C.bridge_encoder_set_complexity(enc.ptr, C.opus_int32(complexity)))
|
||||
}
|
||||
|
||||
// DTX says if discontinuous transmission is enabled.
|
||||
func (enc *LibOpusEncoder) DTX() (bool, error) {
|
||||
var dtx C.opus_int32
|
||||
res := C.bridge_encoder_get_dtx(enc.ptr, &dtx)
|
||||
return dtx > 0, unwrap(res)
|
||||
}
|
||||
|
||||
// SetDTX switches discontinuous transmission.
|
||||
func (enc *LibOpusEncoder) SetDTX(dtx bool) error {
|
||||
var i int
|
||||
if dtx {
|
||||
i = 1
|
||||
}
|
||||
return unwrap(C.bridge_encoder_set_dtx(enc.ptr, C.opus_int32(i)))
|
||||
}
|
||||
|
||||
// MaxBandwidth returns the maximum allowed bandpass value.
|
||||
func (enc *LibOpusEncoder) MaxBandwidth() (Bandwidth, error) {
|
||||
var b C.opus_int32
|
||||
res := C.bridge_encoder_get_max_bandwidth(enc.ptr, &b)
|
||||
return Bandwidth(b), unwrap(res)
|
||||
}
|
||||
|
||||
// SetMaxBandwidth sets the upper limit of the bandpass.
|
||||
func (enc *LibOpusEncoder) SetMaxBandwidth(b Bandwidth) error {
|
||||
return unwrap(C.bridge_encoder_set_max_bandwidth(enc.ptr, C.opus_int32(b)))
|
||||
}
|
||||
|
||||
// FEC says if forward error correction (FEC) is enabled.
|
||||
func (enc *LibOpusEncoder) FEC() (bool, error) {
|
||||
var fec C.opus_int32
|
||||
res := C.bridge_encoder_get_inband_fec(enc.ptr, &fec)
|
||||
return fec > 0, unwrap(res)
|
||||
}
|
||||
|
||||
// SetFEC switches the forward error correction (FEC).
|
||||
func (enc *LibOpusEncoder) SetFEC(fec bool) error {
|
||||
var i int
|
||||
if fec {
|
||||
i = 1
|
||||
}
|
||||
return unwrap(C.bridge_encoder_set_inband_fec(enc.ptr, C.opus_int32(i)))
|
||||
}
|
||||
|
||||
// PacketLossPerc returns configured packet loss percentage.
|
||||
func (enc *LibOpusEncoder) PacketLossPerc() (int, error) {
|
||||
var lossPerc C.opus_int32
|
||||
res := C.bridge_encoder_get_packet_loss_perc(enc.ptr, &lossPerc)
|
||||
return int(lossPerc), unwrap(res)
|
||||
}
|
||||
|
||||
// SetPacketLossPerc sets expected packet loss percentage.
|
||||
func (enc *LibOpusEncoder) SetPacketLossPerc(lossPerc int) error {
|
||||
return unwrap(C.bridge_encoder_set_packet_loss_perc(enc.ptr, C.opus_int32(lossPerc)))
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return fmt.Sprintf("opus: %v", C.GoString(C.opus_strerror(C.int(e))))
|
||||
}
|
||||
|
||||
func unwrap(error C.int) (err error) {
|
||||
if error < C.OPUS_OK {
|
||||
err = Error(int(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func CodecVersion() string { return C.GoString(C.opus_get_version_string()) }
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
package encoder
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/encoder/yuv"
|
||||
)
|
||||
|
||||
type VideoPipe struct {
|
||||
Input chan InFrame
|
||||
Output chan OutFrame
|
||||
done chan struct{}
|
||||
|
||||
encoder Encoder
|
||||
|
||||
// frame size
|
||||
w, h int
|
||||
}
|
||||
|
||||
// NewVideoPipe returns new video encoder pipe.
|
||||
// By default, it waits for RGBA images on the input channel,
|
||||
// converts them into YUV I420 format,
|
||||
// encodes with provided video encoder, and
|
||||
// puts the result into the output channel.
|
||||
func NewVideoPipe(enc Encoder, w, h int) *VideoPipe {
|
||||
return &VideoPipe{
|
||||
Input: make(chan InFrame, 1),
|
||||
Output: make(chan OutFrame, 2),
|
||||
done: make(chan struct{}),
|
||||
|
||||
encoder: enc,
|
||||
|
||||
w: w,
|
||||
h: h,
|
||||
}
|
||||
}
|
||||
|
||||
// Start begins video encoding pipe.
|
||||
// Should be wrapped into a goroutine.
|
||||
func (vp *VideoPipe) Start() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Println("Warn: Recovered panic in encoding ", r)
|
||||
}
|
||||
close(vp.Output)
|
||||
close(vp.done)
|
||||
}()
|
||||
|
||||
yuvProc := yuv.NewYuvImgProcessor(vp.w, vp.h)
|
||||
for img := range vp.Input {
|
||||
yCbCr := yuvProc.Process(img.Image).Get()
|
||||
frame := vp.encoder.Encode(yCbCr)
|
||||
if len(frame) > 0 {
|
||||
vp.Output <- OutFrame{Data: frame, Duration: img.Duration}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (vp *VideoPipe) Stop() {
|
||||
close(vp.Input)
|
||||
<-vp.done
|
||||
if err := vp.encoder.Shutdown(); err != nil {
|
||||
log.Println("error: failed to close the encoder")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package encoder
|
||||
|
||||
import (
|
||||
"image"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InFrame struct {
|
||||
Image *image.RGBA
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type OutFrame struct {
|
||||
Data []byte
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type Encoder interface {
|
||||
Encode(input []byte) []byte
|
||||
Shutdown() error
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package vpx
|
||||
|
||||
type Options struct {
|
||||
// Target bandwidth to use for this stream, in kilobits per second.
|
||||
Bitrate uint
|
||||
// Force keyframe interval.
|
||||
KeyframeInt uint
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func WithOptions(arg Options) Option {
|
||||
return func(args *Options) {
|
||||
args.Bitrate = arg.Bitrate
|
||||
args.KeyframeInt = arg.KeyframeInt
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package yuv
|
||||
|
||||
type Options struct {
|
||||
ChromaP ChromaPos
|
||||
Threaded bool
|
||||
Threads int
|
||||
}
|
||||
|
||||
func (o *Options) override(options ...Option) {
|
||||
for _, opt := range options {
|
||||
opt(o)
|
||||
}
|
||||
}
|
||||
|
||||
type Option func(*Options)
|
||||
|
||||
func Threaded(t bool) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Threaded = t
|
||||
}
|
||||
}
|
||||
|
||||
func Threads(t int) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Threads = t
|
||||
}
|
||||
}
|
||||
|
||||
func ChromaP(cp ChromaPos) Option {
|
||||
return func(opts *Options) {
|
||||
opts.ChromaP = cp
|
||||
}
|
||||
}
|
||||
|
||||
// WithOptions used for config files.
|
||||
func WithOptions(arg Options) Option {
|
||||
return func(args *Options) {
|
||||
args.ChromaP = arg.ChromaP
|
||||
args.Threaded = arg.Threaded
|
||||
args.Threads = arg.Threads
|
||||
}
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
#include "yuv.h"
|
||||
|
||||
// based on: https://stackoverflow.com/questions/9465815/rgb-to-yuv420-algorithm-efficiency
|
||||
|
||||
// Converts RGBA image to YUV (I420) with BT.601 studio color range.
|
||||
void rgbaToYuv(void *destination, void *source, int width, int height, chromaPos chroma) {
|
||||
const int image_size = width * height;
|
||||
unsigned char *rgba = source;
|
||||
unsigned char *dst_y = destination;
|
||||
unsigned char *dst_u = destination + image_size;
|
||||
unsigned char *dst_v = destination + image_size + image_size / 4;
|
||||
|
||||
int r1, g1, b1, stride;
|
||||
// Y plane
|
||||
for (int y = 0; y < height; ++y) {
|
||||
stride = 4 * y * width;
|
||||
for (int x = 0; x < width; ++x) {
|
||||
r1 = 4 * x + stride;
|
||||
g1 = r1 + 1;
|
||||
b1 = g1 + 1;
|
||||
*dst_y++ = ((66 * rgba[r1] + 129 * rgba[g1] + 25 * rgba[b1]) >> 8) + 16;
|
||||
}
|
||||
}
|
||||
|
||||
// U+V plane
|
||||
if (chroma == TOP_LEFT) {
|
||||
for (int y = 0; y < height; y += 2) {
|
||||
stride = 4 * y * width;
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
r1 = 4 * x + stride;
|
||||
g1 = r1 + 1;
|
||||
b1 = g1 + 1;
|
||||
*dst_u++ = ((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) + 128;
|
||||
*dst_v++ = ((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) + 128;
|
||||
}
|
||||
}
|
||||
} else if (chroma == BETWEEN_FOUR) {
|
||||
int r2, g2, b2, r3, g3, b3, r4, g4, b4;
|
||||
|
||||
for (int y = 0; y < height; y += 2) {
|
||||
stride = 4 * y * width;
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
// (1 2) x x
|
||||
// x x x x
|
||||
r1 = 4 * x + stride;
|
||||
g1 = r1 + 1;
|
||||
b1 = g1 + 1;
|
||||
r2 = r1 + 4;
|
||||
g2 = r2 + 1;
|
||||
b2 = g2 + 1;
|
||||
// x x x x
|
||||
// (3 4) x x
|
||||
r3 = r1 + 4 * width;
|
||||
|
||||
g3 = r3 + 1;
|
||||
b3 = g3 + 1;
|
||||
r4 = r3 + 4;
|
||||
g4 = r4 + 1;
|
||||
b4 = g4 + 1;
|
||||
*dst_u++ = (((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) +
|
||||
((-38 * rgba[r2] + -74 * rgba[g2] + 112 * rgba[b2]) >> 8) +
|
||||
((-38 * rgba[r3] + -74 * rgba[g3] + 112 * rgba[b3]) >> 8) +
|
||||
((-38 * rgba[r4] + -74 * rgba[g4] + 112 * rgba[b4]) >> 8) + 512) >> 2;
|
||||
*dst_v++ = (((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) +
|
||||
((112 * rgba[r2] + -94 * rgba[g2] + -18 * rgba[b2]) >> 8) +
|
||||
((112 * rgba[r3] + -94 * rgba[g3] + -18 * rgba[b3]) >> 8) +
|
||||
((112 * rgba[r4] + -94 * rgba[g4] + -18 * rgba[b4]) >> 8) + 512) >> 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void chroma(void *destination, void *source, int pos, int deu, int dev, int width, int height, chromaPos chroma) {
|
||||
unsigned char *rgba = source + 4 * pos;
|
||||
unsigned char *dst_u = destination + deu + pos / 4;
|
||||
unsigned char *dst_v = destination + dev + pos / 4;
|
||||
|
||||
int r1, g1, b1, stride;
|
||||
|
||||
// U+V plane
|
||||
if (chroma == TOP_LEFT) {
|
||||
for (int y = 0; y < height; y += 2) {
|
||||
stride = 4 * y * width;
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
r1 = 4 * x + stride;
|
||||
g1 = r1 + 1;
|
||||
b1 = g1 + 1;
|
||||
*dst_u++ = ((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) + 128;
|
||||
*dst_v++ = ((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) + 128;
|
||||
}
|
||||
}
|
||||
} else if (chroma == BETWEEN_FOUR) {
|
||||
int r2, g2, b2, r3, g3, b3, r4, g4, b4;
|
||||
|
||||
for (int y = 0; y < height; y += 2) {
|
||||
stride = 4 * y * width;
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
// (1 2) x x
|
||||
// x x x x
|
||||
r1 = 4 * x + stride;
|
||||
g1 = r1 + 1;
|
||||
b1 = g1 + 1;
|
||||
r2 = r1 + 4;
|
||||
g2 = r2 + 1;
|
||||
b2 = g2 + 1;
|
||||
// x x x x
|
||||
// (3 4) x x
|
||||
r3 = r1 + 4 * width;
|
||||
g3 = r3 + 1;
|
||||
b3 = g3 + 1;
|
||||
r4 = r3 + 4;
|
||||
g4 = r4 + 1;
|
||||
b4 = g4 + 1;
|
||||
*dst_u++ = (((-38 * rgba[r1] + -74 * rgba[g1] + 112 * rgba[b1]) >> 8) +
|
||||
((-38 * rgba[r2] + -74 * rgba[g2] + 112 * rgba[b2]) >> 8) +
|
||||
((-38 * rgba[r3] + -74 * rgba[g3] + 112 * rgba[b3]) >> 8) +
|
||||
((-38 * rgba[r4] + -74 * rgba[g4] + 112 * rgba[b4]) >> 8) + 512) >> 2;
|
||||
*dst_v++ = (((112 * rgba[r1] + -94 * rgba[g1] + -18 * rgba[b1]) >> 8) +
|
||||
((112 * rgba[r2] + -94 * rgba[g2] + -18 * rgba[b2]) >> 8) +
|
||||
((112 * rgba[r3] + -94 * rgba[g3] + -18 * rgba[b3]) >> 8) +
|
||||
((112 * rgba[r4] + -94 * rgba[g4] + -18 * rgba[b4]) >> 8) + 512) >> 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void luma(void *destination, void *source, int pos, int width, int height) {
|
||||
unsigned char *rgba = source + 4 * pos;
|
||||
unsigned char *dst_y = destination + pos;
|
||||
|
||||
int x, y, r1, g1, b1, stride;
|
||||
|
||||
// Y plane
|
||||
for (y = 0; y < height; ++y) {
|
||||
stride = 4 * y * width;
|
||||
for (x = 0; x < width; ++x) {
|
||||
r1 = 4 * x + stride;
|
||||
g1 = r1 + 1;
|
||||
b1 = g1 + 1;
|
||||
*dst_y++ = 16 + ((66 * rgba[r1] + 129 * rgba[g1] + 25 * rgba[b1]) >> 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
package yuv
|
||||
|
||||
import (
|
||||
"image"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -Wall -O3
|
||||
#include "yuv.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
type ImgProcessor interface {
|
||||
Process(rgba *image.RGBA) ImgProcessor
|
||||
Get() []byte
|
||||
}
|
||||
|
||||
type processor struct {
|
||||
Data []byte
|
||||
w, h int
|
||||
pos ChromaPos
|
||||
|
||||
// cache
|
||||
dst unsafe.Pointer
|
||||
ww C.int
|
||||
chroma C.chromaPos
|
||||
}
|
||||
|
||||
type threadedProcessor struct {
|
||||
*processor
|
||||
|
||||
// threading
|
||||
threads int
|
||||
chunk int
|
||||
|
||||
// cache
|
||||
chromaU C.int
|
||||
chromaV C.int
|
||||
}
|
||||
|
||||
type ChromaPos uint8
|
||||
|
||||
const (
|
||||
TopLeft ChromaPos = iota
|
||||
BetweenFour
|
||||
)
|
||||
|
||||
// NewYuvImgProcessor creates new YUV image converter from RGBA.
|
||||
func NewYuvImgProcessor(w, h int, options ...Option) ImgProcessor {
|
||||
opts := &Options{
|
||||
ChromaP: BetweenFour,
|
||||
Threaded: true,
|
||||
Threads: runtime.NumCPU(),
|
||||
}
|
||||
opts.override(options...)
|
||||
|
||||
bufSize := int(float32(w*h) * 1.5)
|
||||
buf := make([]byte, bufSize)
|
||||
|
||||
processor := processor{
|
||||
Data: buf,
|
||||
chroma: C.chromaPos(opts.ChromaP),
|
||||
dst: unsafe.Pointer(&buf[0]),
|
||||
h: h,
|
||||
pos: opts.ChromaP,
|
||||
w: w,
|
||||
ww: C.int(w),
|
||||
}
|
||||
|
||||
if opts.Threaded {
|
||||
// chunks the image evenly
|
||||
chunk := h / opts.Threads
|
||||
if chunk%2 != 0 {
|
||||
chunk--
|
||||
}
|
||||
|
||||
return &threadedProcessor{
|
||||
chromaU: C.int(w * h),
|
||||
chromaV: C.int(w*h + w*h/4),
|
||||
chunk: chunk,
|
||||
processor: &processor,
|
||||
threads: opts.Threads,
|
||||
}
|
||||
}
|
||||
return &processor
|
||||
}
|
||||
|
||||
func (yuv *processor) Get() []byte {
|
||||
return yuv.Data
|
||||
}
|
||||
|
||||
// Process converts RGBA colorspace into YUV I420 format inside the internal buffer.
|
||||
// Non-threaded version.
|
||||
func (yuv *processor) Process(rgba *image.RGBA) ImgProcessor {
|
||||
C.rgbaToYuv(yuv.dst, unsafe.Pointer(&rgba.Pix[0]), yuv.ww, C.int(yuv.h), yuv.chroma)
|
||||
return yuv
|
||||
}
|
||||
|
||||
func (yuv *threadedProcessor) Get() []byte {
|
||||
return yuv.Data
|
||||
}
|
||||
|
||||
// Process converts RGBA colorspace into YUV I420 format inside the internal buffer.
|
||||
// Threaded version.
|
||||
//
|
||||
// We divide the input image into chunks by the number of available CPUs.
|
||||
// Each chunk should contain 2, 4, 6, and etc. rows of the image.
|
||||
//
|
||||
// 8x4 CPU (2)
|
||||
// x x x x x x x x | Coroutine 1
|
||||
// x x x x x x x x | Coroutine 1
|
||||
// x x x x x x x x | Coroutine 2
|
||||
// x x x x x x x x | Coroutine 2
|
||||
//
|
||||
func (yuv *threadedProcessor) Process(rgba *image.RGBA) ImgProcessor {
|
||||
src := unsafe.Pointer(&rgba.Pix[0])
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2 * yuv.threads)
|
||||
for i := 0; i < yuv.threads; i++ {
|
||||
pos, hh := C.int(yuv.w*i*yuv.chunk), C.int(yuv.chunk)
|
||||
// we need to know how many pixels left
|
||||
// if the image can't be divided evenly
|
||||
// between all the threads
|
||||
if i == yuv.threads-1 {
|
||||
hh = C.int(yuv.h - i*yuv.chunk)
|
||||
}
|
||||
go func() { defer wg.Done(); C.luma(yuv.dst, src, pos, yuv.ww, hh) }()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
C.chroma(yuv.dst, src, pos, yuv.chromaU, yuv.chromaV, yuv.ww, hh, yuv.chroma)
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return yuv
|
||||
}
|
||||
42
pkg/games/launcher.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package games
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Launcher interface {
|
||||
FindAppByName(name string) (AppMeta, error)
|
||||
ExtractAppNameFromUrl(name string) string
|
||||
GetAppNames() []string
|
||||
}
|
||||
|
||||
type AppMeta struct {
|
||||
Name string
|
||||
Type string
|
||||
Base string
|
||||
Path string
|
||||
}
|
||||
|
||||
type GameLauncher struct {
|
||||
lib GameLibrary
|
||||
}
|
||||
|
||||
func NewGameLauncher(lib GameLibrary) GameLauncher { return GameLauncher{lib: lib} }
|
||||
|
||||
func (gl GameLauncher) FindAppByName(name string) (AppMeta, error) {
|
||||
game := gl.lib.FindGameByName(name)
|
||||
if game.Path == "" {
|
||||
return AppMeta{}, fmt.Errorf("couldn't find game info for the game %v", name)
|
||||
}
|
||||
return AppMeta{Name: game.Name, Base: game.Base, Type: game.Type, Path: game.Path}, nil
|
||||
}
|
||||
|
||||
func (gl GameLauncher) ExtractAppNameFromUrl(name string) string {
|
||||
return GetGameNameFromRoomID(name)
|
||||
}
|
||||
|
||||
func (gl GameLauncher) GetAppNames() []string {
|
||||
var gameList []string
|
||||
for _, game := range gl.lib.GetAll() {
|
||||
gameList = append(gameList, game.Name)
|
||||
}
|
||||
return gameList
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
|
@ -12,6 +11,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
)
|
||||
|
||||
// Config is an external configuration
|
||||
|
|
@ -49,6 +49,7 @@ type library struct {
|
|||
// game name -> game meta
|
||||
// games with duplicate names are merged
|
||||
games map[string]GameMetadata
|
||||
log *logger.Logger
|
||||
|
||||
// to restrict parallel execution
|
||||
// or throttling
|
||||
|
|
@ -79,16 +80,18 @@ type GameMetadata struct {
|
|||
Path string
|
||||
}
|
||||
|
||||
func (g GameMetadata) FullPath() string { return filepath.Join(g.Base, g.Path) }
|
||||
|
||||
func (c Config) GetSupportedExtensions() []string { return c.Supported }
|
||||
|
||||
func NewLib(conf Config) GameLibrary { return NewLibWhitelisted(conf, conf) }
|
||||
func NewLib(conf Config, log *logger.Logger) GameLibrary { return NewLibWhitelisted(conf, conf, log) }
|
||||
|
||||
func NewLibWhitelisted(conf Config, filter FileExtensionWhitelist) GameLibrary {
|
||||
func NewLibWhitelisted(conf Config, filter FileExtensionWhitelist, log *logger.Logger) GameLibrary {
|
||||
hasSource := true
|
||||
dir, err := filepath.Abs(conf.BasePath)
|
||||
if err != nil {
|
||||
hasSource = false
|
||||
log.Printf("[lib] invalid source: %v (%v)\n", conf.BasePath, err)
|
||||
log.Error().Err(err).Str("dir", conf.BasePath).Msg("Lib has invalid source")
|
||||
}
|
||||
|
||||
if len(conf.Supported) == 0 {
|
||||
|
|
@ -106,6 +109,7 @@ func NewLibWhitelisted(conf Config, filter FileExtensionWhitelist) GameLibrary {
|
|||
mu: sync.Mutex{},
|
||||
games: map[string]GameMetadata{},
|
||||
hasSource: hasSource,
|
||||
log: log,
|
||||
}
|
||||
|
||||
if conf.WatchMode && hasSource {
|
||||
|
|
@ -135,7 +139,7 @@ func (lib *library) FindGameByName(name string) GameMetadata {
|
|||
|
||||
func (lib *library) Scan() {
|
||||
if !lib.hasSource {
|
||||
log.Printf("[lib] scan... skipped (no source)\n")
|
||||
lib.log.Info().Msg("Lib scan... skipped (no source)")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -144,13 +148,13 @@ func (lib *library) Scan() {
|
|||
if lib.isScanning {
|
||||
defer lib.mu.Unlock()
|
||||
lib.isScanningDelayed = true
|
||||
log.Printf("[lib] scan... delayed\n")
|
||||
lib.log.Debug().Msg("Lib scan... delayed")
|
||||
return
|
||||
}
|
||||
lib.isScanning = true
|
||||
lib.mu.Unlock()
|
||||
|
||||
log.Printf("[lib] scan... started\n")
|
||||
lib.log.Debug().Msg("Lib scan... started")
|
||||
|
||||
start := time.Now()
|
||||
var games []GameMetadata
|
||||
|
|
@ -172,7 +176,7 @@ func (lib *library) Scan() {
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[lib] scan error with %q: %v\n", dir, err)
|
||||
lib.log.Error().Err(err).Str("dir", dir).Msgf("Lib scan error")
|
||||
}
|
||||
|
||||
if len(games) > 0 {
|
||||
|
|
@ -193,7 +197,7 @@ func (lib *library) Scan() {
|
|||
go lib.Scan()
|
||||
}
|
||||
|
||||
log.Printf("[lib] scan... completed\n")
|
||||
lib.log.Info().Msg("Lib scan... completed")
|
||||
}
|
||||
|
||||
// watch adds the ability to rescan the entire library
|
||||
|
|
@ -202,7 +206,7 @@ func (lib *library) Scan() {
|
|||
func (lib *library) watch() {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Printf("[lib] watcher has failed: %v", err)
|
||||
lib.log.Error().Err(err).Msg("Lib watcher has failed")
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -228,11 +232,11 @@ func (lib *library) watch() {
|
|||
}(lib)
|
||||
|
||||
if err = watcher.Add(lib.config.path); err != nil {
|
||||
log.Printf("[lib] watch error %v", err)
|
||||
lib.log.Error().Err(err).Msg("Lib watch error")
|
||||
}
|
||||
<-done
|
||||
_ = watcher.Close()
|
||||
log.Printf("[lib] the watch has ended\n")
|
||||
lib.log.Info().Msg("Lib watch has ended")
|
||||
}
|
||||
|
||||
func (lib *library) set(games []GameMetadata) {
|
||||
|
|
@ -271,14 +275,14 @@ func (lib *library) dumpLibrary() {
|
|||
gameList.WriteString(" " + game.Name + " (" + game.Path + ")" + "\n")
|
||||
}
|
||||
|
||||
log.Printf("\n"+
|
||||
lib.log.Debug().Msgf("Lib dump\n"+
|
||||
"--------------------------------------------\n"+
|
||||
"--- The Library of ROMs ---\n"+
|
||||
"--------------------------------------------\n"+
|
||||
"%v"+
|
||||
"--------------------------------------------\n"+
|
||||
"--- ROMs: %03d %26s ---\n"+
|
||||
"--------------------------------------------\n",
|
||||
"--------------------------------------------",
|
||||
gameList.String(), len(lib.games), lib.lastScanDuration)
|
||||
}
|
||||
|
||||
|
|
@ -2,6 +2,8 @@ package games
|
|||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/logger"
|
||||
)
|
||||
|
||||
func TestLibraryScan(t *testing.T) {
|
||||
|
|
@ -17,12 +19,13 @@ func TestLibraryScan(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
l := logger.NewConsole(false, "w", true)
|
||||
for _, test := range tests {
|
||||
library := NewLib(Config{
|
||||
BasePath: test.directory,
|
||||
Supported: []string{"gba", "zip", "nes"},
|
||||
Ignored: []string{"neogeo", "pgm"},
|
||||
})
|
||||
}, l)
|
||||
library.Scan()
|
||||
games := library.GetAll()
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package session
|
||||
package games
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
const separator = "___"
|
||||
|
||||
// getGameNameFromRoomID parse roomID to get roomID and gameName
|
||||
// GetGameNameFromRoomID parse roomID to get roomID and gameName.
|
||||
func GetGameNameFromRoomID(roomID string) string {
|
||||
parts := strings.Split(roomID, separator)
|
||||
if len(parts) > 1 {
|
||||
|
|
@ -17,11 +17,10 @@ func GetGameNameFromRoomID(roomID string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// generateRoomID generate a unique room ID containing 16 digits
|
||||
// GenerateRoomID generate a unique room ID containing 16 digits.
|
||||
func GenerateRoomID(gameName string) string {
|
||||
// RoomID contains random number + gameName
|
||||
// Next time when we only get roomID, we can launch game based on gameName
|
||||
roomID := strconv.FormatInt(rand.Int63(), 16) + separator + gameName
|
||||
return roomID
|
||||
}
|
||||
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package ice
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
|
||||
)
|
||||
|
||||
type Replacement struct {
|
||||
From string
|
||||
To string
|
||||
}
|
||||
|
||||
func NewIceServer(url string) webrtc.IceServer {
|
||||
return webrtc.IceServer{
|
||||
Url: url,
|
||||
}
|
||||
}
|
||||
|
||||
func NewIceServerCredentials(url string, user string, credential string) webrtc.IceServer {
|
||||
return webrtc.IceServer{
|
||||
Url: url,
|
||||
Username: user,
|
||||
Credential: credential,
|
||||
}
|
||||
}
|
||||
|
||||
func ToJson(iceServers []webrtc.IceServer, replacements ...Replacement) string {
|
||||
var sb strings.Builder
|
||||
sn, n := len(iceServers), len(replacements)
|
||||
if sn > 0 {
|
||||
sb.Grow(sn * 64)
|
||||
}
|
||||
sb.WriteString("[")
|
||||
for i, ice := range iceServers {
|
||||
if i > 0 {
|
||||
sb.WriteString(",{")
|
||||
} else {
|
||||
sb.WriteString("{")
|
||||
}
|
||||
if n > 0 {
|
||||
for _, replacement := range replacements {
|
||||
ice.Url = strings.Replace(ice.Url, "{"+replacement.From+"}", replacement.To, -1)
|
||||
}
|
||||
}
|
||||
sb.WriteString("\"urls\":\"" + ice.Url + "\"")
|
||||
if ice.Username != "" {
|
||||
sb.WriteString(",\"username\":\"" + ice.Username + "\"")
|
||||
}
|
||||
if ice.Credential != "" {
|
||||
sb.WriteString(",\"credential\":\"" + ice.Credential + "\"")
|
||||
}
|
||||
sb.WriteString("}")
|
||||
}
|
||||
sb.WriteString("]")
|
||||
return sb.String()
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
package ice
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/giongto35/cloud-game/v2/pkg/config/webrtc"
|
||||
)
|
||||
|
||||
func TestIce(t *testing.T) {
|
||||
tests := []struct {
|
||||
input []webrtc.IceServer
|
||||
replacements []Replacement
|
||||
output string
|
||||
}{
|
||||
{
|
||||
input: []webrtc.IceServer{
|
||||
NewIceServer("stun:stun.l.google.com:19302"),
|
||||
NewIceServer("stun:{server-ip}:3478"),
|
||||
NewIceServerCredentials("turn:{server-ip}:3478", "root", "root"),
|
||||
},
|
||||
replacements: []Replacement{
|
||||
{
|
||||
From: "server-ip",
|
||||
To: "localhost",
|
||||
},
|
||||
},
|
||||
output: "[" +
|
||||
"{\"urls\":\"stun:stun.l.google.com:19302\"}," +
|
||||
"{\"urls\":\"stun:localhost:3478\"}," +
|
||||
"{\"urls\":\"turn:localhost:3478\",\"username\":\"root\",\"credential\":\"root\"}" +
|
||||
"]",
|
||||
},
|
||||
{
|
||||
input: []webrtc.IceServer{
|
||||
NewIceServer("stun:stun.l.google.com:19302"),
|
||||
},
|
||||
output: "[{\"urls\":\"stun:stun.l.google.com:19302\"}]",
|
||||
},
|
||||
{
|
||||
input: []webrtc.IceServer{},
|
||||
replacements: []Replacement{},
|
||||
output: "[]",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := ToJson(test.input, test.replacements...)
|
||||
|
||||
if result != test.output {
|
||||
t.Errorf("Not exactly what is expected")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIces(b *testing.B) {
|
||||
benches := []struct {
|
||||
name string
|
||||
f func(iceServers []webrtc.IceServer, replacements ...Replacement) string
|
||||
}{
|
||||
{name: "toJson", f: ToJson},
|
||||
}
|
||||
servers := []webrtc.IceServer{
|
||||
NewIceServer("stun:stun.l.google.com:19302"),
|
||||
NewIceServer("stun:{server-ip}:3478"),
|
||||
NewIceServerCredentials("turn:{server-ip}:3478", "root", "root"),
|
||||
}
|
||||
replacements := []Replacement{
|
||||
{From: "server-ip", To: "localhost"},
|
||||
}
|
||||
|
||||
for _, bench := range benches {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
for n := 0; n < b.N; n++ {
|
||||
bench.f(servers, replacements...)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package lock
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TimeLock struct {
|
||||
l chan struct{}
|
||||
locked int32
|
||||
}
|
||||
|
||||
// NewLock returns new lock (mutex) with a timeout option.
|
||||
func NewLock() *TimeLock {
|
||||
return &TimeLock{l: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
// Lock unconditionally blocks the execution.
|
||||
func (tl *TimeLock) Lock() {
|
||||
if tl.isLocked() {
|
||||
return
|
||||
}
|
||||
tl.lock()
|
||||
<-tl.l
|
||||
}
|
||||
|
||||
// LockFor blocks the execution at most for
|
||||
// the given period of time.
|
||||
func (tl *TimeLock) LockFor(d time.Duration) {
|
||||
tl.lock()
|
||||
select {
|
||||
case <-tl.l:
|
||||
case <-time.After(d):
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock removes the current block if any.
|
||||
func (tl *TimeLock) Unlock() {
|
||||
if !tl.isLocked() {
|
||||
return
|
||||
}
|
||||
tl.unlock()
|
||||
tl.l <- struct{}{}
|
||||
}
|
||||
|
||||
func (tl *TimeLock) isLocked() bool { return atomic.LoadInt32(&tl.locked) == 1 }
|
||||
func (tl *TimeLock) lock() { atomic.StoreInt32(&tl.locked, 1) }
|
||||
func (tl *TimeLock) unlock() { atomic.StoreInt32(&tl.locked, 0) }
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package lock
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLock(t *testing.T) {
|
||||
a := 1
|
||||
lock := NewLock()
|
||||
wait := time.Millisecond * 10
|
||||
|
||||
lock.Unlock()
|
||||
lock.Unlock()
|
||||
lock.Unlock()
|
||||
|
||||
go func(timeLock *TimeLock) {
|
||||
time.Sleep(time.Second * 1)
|
||||
lock.Unlock()
|
||||
}(lock)
|
||||
|
||||
lock.LockFor(time.Second * 30)
|
||||
lock.LockFor(wait)
|
||||
lock.LockFor(wait)
|
||||
lock.LockFor(wait)
|
||||
lock.LockFor(wait)
|
||||
lock.LockFor(time.Millisecond * 10)
|
||||
go func(timeLock *TimeLock) {
|
||||
time.Sleep(time.Millisecond * 1)
|
||||
lock.Unlock()
|
||||
}(lock)
|
||||
lock.Lock()
|
||||
|
||||
a -= 1
|
||||
if a != 0 {
|
||||
t.Errorf("lock test failed because a != 0")
|
||||
}
|
||||
}
|
||||
181
pkg/logger/logger.go
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// Level defines log levels.
|
||||
type Level int8
|
||||
|
||||
const (
|
||||
DebugLevel Level = iota
|
||||
InfoLevel
|
||||
WarnLevel
|
||||
ErrorLevel
|
||||
FatalLevel
|
||||
PanicLevel
|
||||
NoLevel
|
||||
Disabled
|
||||
TraceLevel Level = -1
|
||||
// Values less than TraceLevel are handled as numbers.
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case TraceLevel:
|
||||
return zerolog.LevelTraceValue
|
||||
case DebugLevel:
|
||||
return zerolog.LevelDebugValue
|
||||
case InfoLevel:
|
||||
return zerolog.LevelInfoValue
|
||||
case WarnLevel:
|
||||
return zerolog.LevelWarnValue
|
||||
case ErrorLevel:
|
||||
return zerolog.LevelErrorValue
|
||||
case FatalLevel:
|
||||
return zerolog.LevelFatalValue
|
||||
case PanicLevel:
|
||||
return zerolog.LevelPanicValue
|
||||
case Disabled:
|
||||
return "disabled"
|
||||
case NoLevel:
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(int(l))
|
||||
}
|
||||
|
||||
var pid = os.Getpid()
|
||||
|
||||
type Logger struct {
|
||||
logger *zerolog.Logger
|
||||
}
|
||||
|
||||
func New(isDebug bool) *Logger {
|
||||
logLevel := zerolog.InfoLevel
|
||||
if isDebug {
|
||||
logLevel = zerolog.DebugLevel
|
||||
}
|
||||
zerolog.SetGlobalLevel(logLevel)
|
||||
logger := zerolog.New(os.Stderr).With().Timestamp().Fields(map[string]any{"pid": pid}).Logger()
|
||||
return &Logger{logger: &logger}
|
||||
}
|
||||
|
||||
func NewConsole(isDebug bool, tag string, noColor bool) *Logger {
|
||||
logLevel := zerolog.InfoLevel
|
||||
if isDebug {
|
||||
logLevel = zerolog.DebugLevel
|
||||
}
|
||||
zerolog.SetGlobalLevel(logLevel)
|
||||
zerolog.TimeFieldFormat = time.RFC3339Nano
|
||||
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: "15:04:05.0000", NoColor: noColor,
|
||||
PartsOrder: []string{
|
||||
zerolog.TimestampFieldName,
|
||||
"pid",
|
||||
zerolog.LevelFieldName,
|
||||
zerolog.CallerFieldName,
|
||||
"s",
|
||||
"d",
|
||||
"c",
|
||||
"m",
|
||||
zerolog.MessageFieldName,
|
||||
},
|
||||
FieldsExclude: []string{"s", "c", "d", "m", "pid"},
|
||||
}
|
||||
|
||||
if output.NoColor {
|
||||
output.FormatMessage = func(i any) string {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%v", i)
|
||||
}
|
||||
}
|
||||
|
||||
//multi := zerolog.MultiLevelWriter(output, os.Stdout)
|
||||
logger := zerolog.New(output).With().
|
||||
Str("pid", fmt.Sprintf("%4x", pid)).
|
||||
Str("s", tag).
|
||||
Str("m", "").
|
||||
Str("d", " ").
|
||||
Str("c", " ").
|
||||
// Str("tag", tag). use when a file writer
|
||||
Timestamp().Logger()
|
||||
return &Logger{logger: &logger}
|
||||
}
|
||||
|
||||
func Default() *Logger { return &Logger{logger: &log.Logger} }
|
||||
|
||||
// GetLevel returns the current Level of l.
|
||||
func (l *Logger) GetLevel() Level { return Level(l.logger.GetLevel()) }
|
||||
|
||||
// Output duplicates the global logger and sets w as its output.
|
||||
func (l *Logger) Output(w io.Writer) zerolog.Logger { return l.logger.Output(w) }
|
||||
|
||||
// With creates a child logger with the field added to its context.
|
||||
func (l *Logger) With() zerolog.Context { return l.logger.With() }
|
||||
|
||||
// Level creates a child logger with the minimum accepted level set to level.
|
||||
func (l *Logger) Level(level Level) zerolog.Logger { return l.logger.Level(zerolog.Level(level)) }
|
||||
|
||||
// Sample returns a logger with the s sampler.
|
||||
func (l *Logger) Sample(s zerolog.Sampler) zerolog.Logger { return l.logger.Sample(s) }
|
||||
|
||||
// Hook returns a logger with the h Hook.
|
||||
func (l *Logger) Hook(h zerolog.Hook) zerolog.Logger { return l.logger.Hook(h) }
|
||||
|
||||
// Debug starts a new message with debug level.
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Debug() *zerolog.Event { return l.logger.Debug() }
|
||||
|
||||
func (l *Logger) Trace() *zerolog.Event { return l.logger.Trace() }
|
||||
|
||||
// Info starts a new message with info level.
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Info() *zerolog.Event { return l.logger.Info() }
|
||||
|
||||
// Warn starts a new message with warn level.
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Warn() *zerolog.Event { return l.logger.Warn() }
|
||||
|
||||
// Error starts a new message with error level.
|
||||
func (l *Logger) Error() *zerolog.Event { return l.logger.Error() }
|
||||
|
||||
// Fatal starts a new message with fatal level. The os.Exit(1) function
|
||||
// is called by the Msg method.
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Fatal() *zerolog.Event { return l.logger.Fatal() }
|
||||
|
||||
// Panic starts a new message with panic level. The message is also sent
|
||||
// to the panic function.
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Panic() *zerolog.Event { return l.logger.Panic() }
|
||||
|
||||
// WithLevel starts a new message with level.
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) WithLevel(level zerolog.Level) *zerolog.Event { return l.logger.WithLevel(level) }
|
||||
|
||||
// Log starts a new message with no level. Setting zerolog.GlobalLevel to
|
||||
// zerolog.Disabled will still disable events produced by this method.
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Log() *zerolog.Event { return l.logger.Log() }
|
||||
|
||||
// Print sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Print.
|
||||
func (l *Logger) Print(v ...any) { l.logger.Print(v...) }
|
||||
|
||||
// Printf sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Printf(format string, v ...any) { l.logger.Printf(format, v...) }
|
||||
|
||||
// Extend adds some additional context to the existing logger.
|
||||
func (l *Logger) Extend(ctx zerolog.Context) *Logger {
|
||||
logger := ctx.Logger()
|
||||
return &Logger{logger: &logger}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package media
|
||||
|
||||
// Buffer is a simple non-thread safe ring buffer for audio samples.
|
||||
// It should be used for 16bit PCM (LE interleaved) data.
|
||||
type (
|
||||
Buffer struct {
|
||||
s Samples
|
||||
wi int
|
||||
}
|
||||
OnFull func(s Samples)
|
||||
Samples []int16
|
||||
)
|
||||
|
||||
func NewBuffer(numSamples int) Buffer { return Buffer{s: make(Samples, numSamples)} }
|
||||
|
||||
// Write fills the buffer with data calling a callback function when
|
||||
// the internal buffer fills out.
|
||||
//
|
||||
// Consider two cases:
|
||||
//
|
||||
// 1. Underflow, when the length of written data is less than the buffer's available space.
|
||||
// 2. Overflow, when the length exceeds the current available buffer space.
|
||||
// In the both cases we overwrite any previous values in the buffer and move the internal
|
||||
// write pointer on the length of written data.
|
||||
// In the first case we won't call the callback, but it will be called every time
|
||||
// when the internal buffer overflows until all samples are read.
|
||||
func (b *Buffer) Write(s Samples, onFull OnFull) (r int) {
|
||||
for r < len(s) {
|
||||
w := copy(b.s[b.wi:], s[r:])
|
||||
r += w
|
||||
b.wi += w
|
||||
if b.wi == len(b.s) {
|
||||
b.wi = 0
|
||||
if onFull != nil {
|
||||
onFull(b.s)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||