diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65e92ea1..e62ef828 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -72,7 +72,7 @@ jobs: ./mesa/systemwidedeploy.cmd < ./commands wget -q https://buildbot.libretro.com/nightly/windows/x86_64/latest/mupen64plus_next_libretro.dll.zip - "/c/Program Files/7-Zip/7z.exe" x mupen64plus_next_libretro.dll.zip -oassets/emulator/libretro/cores + "/c/Program Files/7-Zip/7z.exe" x mupen64plus_next_libretro.dll.zip -oassets/cores make build diff --git a/Dockerfile b/Dockerfile index 9aa489a4..008388f0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -41,8 +41,9 @@ RUN apt-get update && apt-get install -y \ COPY --from=build ${BUILD_PATH}/bin/ ./ RUN cp -s $(pwd)/* /usr/local/bin COPY web ./web -COPY assets/emulator/libretro/cores/*.so \ - assets/emulator/libretro/cores/*.cfg \ - ./assets/emulator/libretro/cores/ +COPY assets/cores/*.so \ + assets/cores/*.cfg \ + ./assets/cores/ +COPY configs ./configs EXPOSE 8000 9000 3478/tcp 3478/udp diff --git a/Makefile b/Makefile index 6e4bab60..46333753 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ dev.build-local: dev.run: dev.build-local ./bin/coordinator --v=5 & ./bin/worker --coordinatorhost localhost:8000 - + dev.run-docker: docker rm cloud-game-local -f || true CLOUD_GAME_GAMES_PATH=$(PWD)/assets/games docker-compose up --build @@ -111,7 +111,7 @@ DLIB_ALTER ?= false CORE_EXT ?= *_libretro.so COORDINATOR_DIR = ./$(RELEASE_DIR) WORKER_DIR = ./$(RELEASE_DIR) -CORES_DIR = assets/emulator/libretro/cores +CORES_DIR = assets/cores GAMES_DIR = assets/games .PHONY: release .SILENT: release diff --git a/README.md b/README.md index 1b9e7c38..24768b31 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,13 @@ It will spawn a docker environment and you can access the service on `localhost: *Note.* Docker for Windows is not supposed to work with provided configuration, use WSL2 instead. +## Configuration + +The configuration parameters are stored in the [`configs/config.yaml`](configs/config.yaml) file which is shared for +all application instances on the same host system. +It is possible to specify individual configuration files for each instance as well as override some parameters, +for that purpose, please refer to the list of command-line options of the apps. + ## Technical Document - [webrtchacks Blog: Open Source Cloud Gaming with WebRTC](https://webrtchacks.com/open-source-cloud-gaming-with-webrtc/) - [Wiki (outdated)](https://github.com/giongto35/cloud-game/wiki) diff --git a/assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so b/assets/cores/mupen64plus_next_libretro.armv7-neon-hf.so old mode 100755 new mode 100644 similarity index 100% rename from assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so rename to assets/cores/mupen64plus_next_libretro.armv7-neon-hf.so diff --git a/assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg b/assets/cores/mupen64plus_next_libretro.cfg similarity index 100% rename from assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg rename to assets/cores/mupen64plus_next_libretro.cfg diff --git a/assets/emulator/libretro/cores/mupen64plus_next_libretro.dylib b/assets/cores/mupen64plus_next_libretro.dylib old mode 100755 new mode 100644 similarity index 100% rename from assets/emulator/libretro/cores/mupen64plus_next_libretro.dylib rename to assets/cores/mupen64plus_next_libretro.dylib diff --git a/assets/cores/pcsx_rearmed_libretro.cfg b/assets/cores/pcsx_rearmed_libretro.cfg new file mode 100644 index 00000000..382324e9 --- /dev/null +++ b/assets/cores/pcsx_rearmed_libretro.cfg @@ -0,0 +1 @@ +pcsx_rearmed_drc = disabled diff --git a/assets/emulator/libretro/cores/citra_libretro.so b/assets/emulator/libretro/cores/citra_libretro.so deleted file mode 100644 index f61458bd..00000000 Binary files a/assets/emulator/libretro/cores/citra_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/desmume_libretro.so b/assets/emulator/libretro/cores/desmume_libretro.so deleted file mode 100644 index 6928637e..00000000 Binary files a/assets/emulator/libretro/cores/desmume_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so deleted file mode 100755 index c67f7c8e..00000000 Binary files a/assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/fbneo_libretro.so b/assets/emulator/libretro/cores/fbneo_libretro.so deleted file mode 100644 index 25853464..00000000 Binary files a/assets/emulator/libretro/cores/fbneo_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_psx_hw_libretro.so b/assets/emulator/libretro/cores/mednafen_psx_hw_libretro.so deleted file mode 100644 index 3d55ac19..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_psx_hw_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_psx_libretro.dll b/assets/emulator/libretro/cores/mednafen_psx_libretro.dll deleted file mode 100644 index cd80c46c..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_psx_libretro.dll and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_psx_libretro.dylib b/assets/emulator/libretro/cores/mednafen_psx_libretro.dylib deleted file mode 100644 index 22c1dc89..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_psx_libretro.dylib and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_psx_libretro.so b/assets/emulator/libretro/cores/mednafen_psx_libretro.so deleted file mode 100644 index cc37c5d3..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_psx_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so deleted file mode 100755 index ff9713c9..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_snes_libretro.dll b/assets/emulator/libretro/cores/mednafen_snes_libretro.dll deleted file mode 100644 index 19e4537d..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_snes_libretro.dll and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_snes_libretro.dylib b/assets/emulator/libretro/cores/mednafen_snes_libretro.dylib deleted file mode 100644 index 27129624..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_snes_libretro.dylib and /dev/null differ diff --git a/assets/emulator/libretro/cores/mednafen_snes_libretro.so b/assets/emulator/libretro/cores/mednafen_snes_libretro.so deleted file mode 100644 index b6f59d4a..00000000 Binary files a/assets/emulator/libretro/cores/mednafen_snes_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mesen_libretro.so b/assets/emulator/libretro/cores/mesen_libretro.so deleted file mode 100644 index 420f1227..00000000 Binary files a/assets/emulator/libretro/cores/mesen_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mess2015_libretro.so b/assets/emulator/libretro/cores/mess2015_libretro.so deleted file mode 100644 index f0055b48..00000000 Binary files a/assets/emulator/libretro/cores/mess2015_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/meteor_libretro.so b/assets/emulator/libretro/cores/meteor_libretro.so deleted file mode 100644 index 2ee8e50c..00000000 Binary files a/assets/emulator/libretro/cores/meteor_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so deleted file mode 100755 index 352e7620..00000000 Binary files a/assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mgba_libretro.dll b/assets/emulator/libretro/cores/mgba_libretro.dll deleted file mode 100644 index 62078069..00000000 Binary files a/assets/emulator/libretro/cores/mgba_libretro.dll and /dev/null differ diff --git a/assets/emulator/libretro/cores/mgba_libretro.dylib b/assets/emulator/libretro/cores/mgba_libretro.dylib deleted file mode 100644 index 6c0ae73a..00000000 Binary files a/assets/emulator/libretro/cores/mgba_libretro.dylib and /dev/null differ diff --git a/assets/emulator/libretro/cores/mgba_libretro.so b/assets/emulator/libretro/cores/mgba_libretro.so deleted file mode 100644 index 76269a29..00000000 Binary files a/assets/emulator/libretro/cores/mgba_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/mupen64plus_next_libretro.so b/assets/emulator/libretro/cores/mupen64plus_next_libretro.so deleted file mode 100755 index b21692cd..00000000 Binary files a/assets/emulator/libretro/cores/mupen64plus_next_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so deleted file mode 100755 index 607397a4..00000000 Binary files a/assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/nestopia_libretro.dll b/assets/emulator/libretro/cores/nestopia_libretro.dll deleted file mode 100644 index e5a9a944..00000000 Binary files a/assets/emulator/libretro/cores/nestopia_libretro.dll and /dev/null differ diff --git a/assets/emulator/libretro/cores/nestopia_libretro.dylib b/assets/emulator/libretro/cores/nestopia_libretro.dylib deleted file mode 100644 index 3a6d7705..00000000 Binary files a/assets/emulator/libretro/cores/nestopia_libretro.dylib and /dev/null differ diff --git a/assets/emulator/libretro/cores/nestopia_libretro.so b/assets/emulator/libretro/cores/nestopia_libretro.so deleted file mode 100644 index d296fcab..00000000 Binary files a/assets/emulator/libretro/cores/nestopia_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so deleted file mode 100755 index 92fcdc25..00000000 Binary files a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.dll b/assets/emulator/libretro/cores/pcsx_rearmed_libretro.dll deleted file mode 100644 index 8c4ab41b..00000000 Binary files a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.dll and /dev/null differ diff --git a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.dylib b/assets/emulator/libretro/cores/pcsx_rearmed_libretro.dylib deleted file mode 100644 index 72d00dfa..00000000 Binary files a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.dylib and /dev/null differ diff --git a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.so b/assets/emulator/libretro/cores/pcsx_rearmed_libretro.so deleted file mode 100644 index 8db51458..00000000 Binary files a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/prosystem_libretro.so b/assets/emulator/libretro/cores/prosystem_libretro.so deleted file mode 100644 index f5b4b08d..00000000 Binary files a/assets/emulator/libretro/cores/prosystem_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/snes9x2010_libretro.so b/assets/emulator/libretro/cores/snes9x2010_libretro.so deleted file mode 100644 index 1517b9cd..00000000 Binary files a/assets/emulator/libretro/cores/snes9x2010_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/snes9x_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/snes9x_libretro.armv7-neon-hf.so deleted file mode 100755 index a0e87118..00000000 Binary files a/assets/emulator/libretro/cores/snes9x_libretro.armv7-neon-hf.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/snes9x_libretro.dll b/assets/emulator/libretro/cores/snes9x_libretro.dll deleted file mode 100755 index 63e6fc91..00000000 Binary files a/assets/emulator/libretro/cores/snes9x_libretro.dll and /dev/null differ diff --git a/assets/emulator/libretro/cores/snes9x_libretro.dylib b/assets/emulator/libretro/cores/snes9x_libretro.dylib deleted file mode 100644 index 3a20f715..00000000 Binary files a/assets/emulator/libretro/cores/snes9x_libretro.dylib and /dev/null differ diff --git a/assets/emulator/libretro/cores/snes9x_libretro.so b/assets/emulator/libretro/cores/snes9x_libretro.so deleted file mode 100644 index e32cdd89..00000000 Binary files a/assets/emulator/libretro/cores/snes9x_libretro.so and /dev/null differ diff --git a/assets/emulator/libretro/cores/yabasanshiro_libretro.so b/assets/emulator/libretro/cores/yabasanshiro_libretro.so deleted file mode 100644 index fc42754d..00000000 Binary files a/assets/emulator/libretro/cores/yabasanshiro_libretro.so and /dev/null differ diff --git a/cmd/coordinator/main.go b/cmd/coordinator/main.go index b7775e3f..a47f2a2d 100644 --- a/cmd/coordinator/main.go +++ b/cmd/coordinator/main.go @@ -2,22 +2,25 @@ package main import ( "context" + goflag "flag" "math/rand" "os" "os/signal" "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/util/logging" "github.com/golang/glog" - "github.com/spf13/pflag" + flag "github.com/spf13/pflag" ) func main() { rand.Seed(time.Now().UTC().UnixNano()) - cfg := coordinator.NewDefaultConfig() - cfg.AddFlags(pflag.CommandLine) + conf := config.NewConfig() + flag.CommandLine.AddGoFlagSet(goflag.CommandLine) + conf.ParseFlags() logging.Init() defer logging.Flush() @@ -25,8 +28,8 @@ func main() { ctx, cancelCtx := context.WithCancel(context.Background()) glog.Infof("Initializing coordinator server") - glog.V(4).Infof("Coordinator configs %v", cfg) - o := coordinator.New(ctx, cfg) + glog.V(4).Infof("Coordinator configs %v", conf) + o := coordinator.New(ctx, conf) if err := o.Run(); err != nil { glog.Errorf("Failed to run coordinator server, reason %v", err) os.Exit(1) diff --git a/cmd/worker/main.go b/cmd/worker/main.go index 999b6828..f5a33672 100644 --- a/cmd/worker/main.go +++ b/cmd/worker/main.go @@ -2,6 +2,7 @@ package main import ( "context" + goflag "flag" "math/rand" "os" "os/signal" @@ -12,14 +13,15 @@ import ( "github.com/giongto35/cloud-game/v2/pkg/util/logging" "github.com/giongto35/cloud-game/v2/pkg/worker" "github.com/golang/glog" - "github.com/spf13/pflag" + flag "github.com/spf13/pflag" ) func run() { rand.Seed(time.Now().UTC().UnixNano()) - cfg := config.NewDefaultConfig() - cfg.AddFlags(pflag.CommandLine) + conf := config.NewConfig() + flag.CommandLine.AddGoFlagSet(goflag.CommandLine) + conf.ParseFlags() logging.Init() defer logging.Flush() @@ -27,8 +29,8 @@ func run() { ctx, cancelCtx := context.WithCancel(context.Background()) glog.Infof("Initializing worker server") - glog.V(4).Infof("Worker configs %v", cfg) - o := worker.New(ctx, cfg) + glog.V(4).Infof("Worker configs %v", conf) + o := worker.New(ctx, conf) if err := o.Run(); err != nil { glog.Errorf("Failed to run worker, reason %v", err) os.Exit(1) diff --git a/configs/config.yaml b/configs/config.yaml new file mode 100644 index 00000000..9ff25ffa --- /dev/null +++ b/configs/config.yaml @@ -0,0 +1,161 @@ +# +# Application configuration file +# + +# application environment (dev, staging, prod) +environment: dev + +coordinator: + # address if the server want to connect directly to debug + debugHost: + # games library + library: + # some directory which is gonna be the 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 + supported: + # a list of ignored words in the ROM filenames + ignored: + - neogeo + - pgm + # print some additional info + verbose: true + # enable library directory live reload + # (experimental) + watchMode: false + monitoring: + port: 6601 + # enable Go profiler HTTP server + profilingEnabled: false + metricEnabled: false + urlPrefix: /coordinator + # the public domain of the coordinator + publicDomain: http://localhost:8000 + # specify the worker address that the client can ping (with protocol and port) + pingServer: + # HTTP(S) server config + server: + port: 8000 + httpsPort: 443 + httpsKey: + httpsChain: + +worker: + network: + # a coordinator address to connect to + coordinatorAddress: localhost:8000 + # ISO Alpha-2 country code to group workers by zones + zone: + monitoring: + # monitoring server port + port: 6601 + profilingEnabled: false + # monitoring server URL prefix + metricEnabled: true + urlPrefix: /worker + server: + port: 9000 + httpsPort: 443 + httpsKey: + httpsChain: + +emulator: + # set output viewport scale factor + scale: 1 + + aspectRatio: + # enable aspect ratio changing + # (experimental) + keep: false + # recalculate emulator game frame size to the given WxH + width: 320 + height: 240 + + libretro: + cores: + paths: + libs: assets/cores + configs: assets/cores + # Config params for Libretro cores repository, + # available types are: + # - buildbot (the default Libretro nightly repository) + # - github (GitHub raw repository with a similar structure to buildbot) + # - raw (just a link to a zip file extracted as is) + repo: + # enable auto-download for the list of cores (list->lib) + sync: true + type: buildbot + url: https://buildbot.libretro.com/nightly + # if repo has file compression + compression: zip + # Libretro core configuration + # + # Available config params: + # - lib (string) + # - config (string) + # - roms ([]string) + # - width (int) + # - height (int) + # - ratio (float) + # - isGlAllowed (bool) + # - usesLibCo (bool) + # - hasMultitap (bool) + list: + gba: + lib: mgba_libretro + roms: [ "gba", "gbc" ] + width: 240 + height: 160 + pcsx: + lib: pcsx_rearmed_libretro + config: pcsx_rearmed_libretro.cfg + roms: [ "cue" ] + width: 350 + height: 240 + nes: + lib: nestopia_libretro + roms: [ "nes" ] + width: 256 + height: 240 + snes: + lib: snes9x_libretro + roms: [ "smc", "sfc", "swc", "fig", "bs" ] + width: 256 + height: 224 + hasMultitap: true + mame: + lib: fbneo_libretro + roms: [ "zip" ] + width: 240 + height: 160 + n64: + lib: mupen64plus_next_libretro + config: mupen64plus_next_libretro.cfg + roms: [ "n64", "v64", "z64" ] + width: 320 + height: 240 + isGlAllowed: true + usesLibCo: true + +encoder: + audio: + channels: 2 + # audio frame duration needed for WebRTC (Opus) + frame: 20 + frequency: 48000 + # run without a game + # (experimental) + withoutGame: false + +webrtc: + # a list of STUN/TURN servers for the client + # {server-ip} placeholder will be replaced with + # the current server IP + iceServers: + - url: stun:stun.l.google.com:19302 + - url: stun:{server-ip}:3478 + - url: turn:{server-ip}:3478 + username: root + credential: root diff --git a/go.mod b/go.mod index 5ecfa87c..8d8bf069 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,17 @@ go 1.13 require ( cloud.google.com/go v0.70.0 // indirect cloud.google.com/go/storage v1.12.0 + github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 github.com/fsnotify/fsnotify v1.4.9 github.com/gen2brain/x264-go v0.0.0-20200605131102-0523307cbe23 github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 + github.com/gofrs/flock v0.8.0 github.com/gofrs/uuid v3.3.0+incompatible github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 + github.com/kkyr/fig v0.2.0 github.com/lucas-clemente/quic-go v0.18.1 // indirect github.com/marten-seemann/qtls-go1-15 v0.1.1 // indirect github.com/pion/dtls/v2 v2.0.3 // indirect @@ -35,4 +38,5 @@ require ( google.golang.org/genproto v0.0.0-20201030142918-24207fddd1c3 // indirect google.golang.org/grpc v1.33.1 // indirect gopkg.in/hraban/opus.v2 v2.0.0-20201025103112-d779bb1cc5a2 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c ) diff --git a/go.sum b/go.sum index 81d17ff9..fb4ec071 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kB github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +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/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= @@ -140,6 +142,8 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= +github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= @@ -281,6 +285,8 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkyr/fig v0.2.0 h1:t/5yENaBw8ATXbQSWpPqwXLCn6wdhEi6jWXRfUgytZI= +github.com/kkyr/fig v0.2.0/go.mod h1:iqSnedEGFSofGzaB8p34xOhX1ppE1kMulSmJLZ2tNnw= 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= @@ -324,6 +330,7 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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= @@ -369,6 +376,8 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -951,6 +960,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 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.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/pkg/config/config.go b/pkg/config/config.go deleted file mode 100644 index 7f9c6323..00000000 --- a/pkg/config/config.go +++ /dev/null @@ -1,113 +0,0 @@ -package config - -import ( - "flag" - "time" - - "github.com/giongto35/cloud-game/v2/pkg/emulator/image" -) - -const DefaultSTUNTURN = `[{"urls":"stun:stun-turn.webgame2d.com:3478"},{"urls":"turn:stun-turn.webgame2d.com:3478","username":"root","credential":"root"}]` -const CODEC_VP8 = "VP8" -const CODEC_H264 = "H264" - -const AUDIO_RATE = 48000 -const AUDIO_CHANNELS = 2 -const AUDIO_MS = 20 -const AUDIO_FRAME = AUDIO_RATE * AUDIO_MS / 1000 * AUDIO_CHANNELS - -var FrontendSTUNTURN = flag.String("stunturn", DefaultSTUNTURN, "Frontend STUN TURN servers") -var Mode = flag.String("mode", "dev", "Environment") -var StunTurnTemplate = `[{"urls":"stun:stun.l.google.com:19302"},{"urls":"stun:%s:3478"},{"urls":"turn:%s:3478","username":"root","credential":"root"}]` -var HttpPort = flag.String("httpPort", "8000", "User agent port of the app") -var HttpsPort = flag.Int("httpsPort", 443, "Https Port") -var HttpsKey = flag.String("httpsKey", "", "Https Key") -var HttpsChain = flag.String("httpsChain", "", "Https Chain") - -var WSWait = 20 * time.Second -var ProdEnv = "prod" -var StagingEnv = "staging" - -var FileTypeToEmulator = map[string]string{ - "gba": "gba", - "gbc": "gba", - "cue": "pcsx", - "zip": "mame", - "nes": "nes", - "smc": "snes", - "sfc": "snes", - "swc": "snes", - "fig": "snes", - "bs": "snes", - "n64": "n64", - "v64": "n64", - "z64": "n64", -} - -var SupportedRomExtensions = listSupportedRomExtensions() - -// There is no good way to determine main width and height of the emulator. -// When game run, frame width and height can scale abnormally. -type EmulatorMeta struct { - Path string - Config string - Width int - Height int - AudioSampleRate int - Fps float64 - BaseWidth int - BaseHeight int - Ratio float64 - Rotation image.Rotate - IsGlAllowed bool - UsesLibCo bool - AutoGlContext bool - HasMultitap bool -} - -var EmulatorConfig = map[string]EmulatorMeta{ - "gba": { - Path: "assets/emulator/libretro/cores/mgba_libretro", - Width: 240, - Height: 160, - }, - "pcsx": { - Path: "assets/emulator/libretro/cores/pcsx_rearmed_libretro", - Width: 350, - Height: 240, - }, - "nes": { - Path: "assets/emulator/libretro/cores/nestopia_libretro", - Width: 256, - Height: 240, - }, - "snes": { - Path: "assets/emulator/libretro/cores/snes9x_libretro", - Width: 256, - Height: 224, - HasMultitap: true, - }, - "mame": { - Path: "assets/emulator/libretro/cores/fbneo_libretro", - Width: 240, - Height: 160, - }, - "n64": { - Path: "assets/emulator/libretro/cores/mupen64plus_next_libretro", - Config: "assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg", - Width: 320, - Height: 240, - IsGlAllowed: true, - UsesLibCo: true, - }, -} - -var EmulatorExtension = []string{".so", ".armv7-neon-hf.so", ".dylib", ".dll"} - -func listSupportedRomExtensions() []string { - m := make([]string, 0, len(FileTypeToEmulator)) - for k := range FileTypeToEmulator { - m = append(m, k) - } - return m -} diff --git a/pkg/config/coordinator/config.go b/pkg/config/coordinator/config.go new file mode 100644 index 00000000..16abaaae --- /dev/null +++ b/pkg/config/coordinator/config.go @@ -0,0 +1,43 @@ +package coordinator + +import ( + "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/shared" + webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc" + "github.com/giongto35/cloud-game/v2/pkg/games" + "github.com/giongto35/cloud-game/v2/pkg/monitoring" + flag "github.com/spf13/pflag" +) + +type Config struct { + Coordinator struct { + PublicDomain string + PingServer string + DebugHost string + Library games.Config + Monitoring monitoring.ServerMonitoringConfig + Server shared.Server + } + Emulator emulator.Emulator + Environment shared.Environment + Webrtc struct { + IceServers []webrtcConfig.IceServer + } +} + +// allows custom config path +var configPath string + +func NewConfig() (conf Config) { + config.LoadConfig(&conf, configPath) + return +} + +func (c *Config) ParseFlags() { + c.Environment.WithFlags() + 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.Parse() +} diff --git a/pkg/config/emulator/config.go b/pkg/config/emulator/config.go new file mode 100644 index 00000000..122d94b6 --- /dev/null +++ b/pkg/config/emulator/config.go @@ -0,0 +1,95 @@ +package emulator + +import ( + "path" + "path/filepath" +) + +type Emulator struct { + Scale int + AspectRatio struct { + Keep bool + Width int + Height int + } + Libretro LibretroConfig +} + +type LibretroConfig struct { + Cores struct { + Paths struct { + Libs string + Configs string + } + Repo struct { + Sync bool + Type string + Url string + Compression string + } + List map[string]LibretroCoreConfig + } +} + +type LibretroCoreConfig struct { + Lib string + Config string + Roms []string + Width int + Height int + Ratio float64 + IsGlAllowed bool + UsesLibCo bool + HasMultitap bool + + // hack: keep it here to pass it down the emulator + AutoGlContext bool +} + +// GetLibretroCoreConfig returns a core config with expanded paths. +func (e *Emulator) GetLibretroCoreConfig(emulator string) LibretroCoreConfig { + cores := e.Libretro.Cores + conf := cores.List[emulator] + conf.Lib = path.Join(cores.Paths.Libs, conf.Lib) + if conf.Config != "" { + conf.Config = path.Join(cores.Paths.Configs, conf.Config) + } + return conf +} + +// GetEmulatorByRom returns emulator name by its supported ROM name. +// !to cache into an optimized data structure +func (e *Emulator) GetEmulatorByRom(rom string) string { + for emu, core := range e.Libretro.Cores.List { + for _, romName := range core.Roms { + if rom == romName { + return emu + } + } + } + return "" +} + +func (e *Emulator) GetSupportedExtensions() []string { + var extensions []string + for _, core := range e.Libretro.Cores.List { + extensions = append(extensions, core.Roms...) + } + return extensions +} + +func (l *LibretroConfig) GetCores() []string { + var cores []string + for _, core := range l.Cores.List { + cores = append(cores, core.Lib) + } + return cores +} + +func (l *LibretroConfig) GetCoresStorePath() string { + pth, err := filepath.Abs(l.Cores.Paths.Libs) + if err != nil { + return "" + } + return pth +} diff --git a/pkg/config/encoder/config.go b/pkg/config/encoder/config.go new file mode 100644 index 00000000..59afee6a --- /dev/null +++ b/pkg/config/encoder/config.go @@ -0,0 +1,16 @@ +package encoder + +type Encoder struct { + Audio Audio + WithoutGame bool +} + +type Audio struct { + Channels int + Frame int + Frequency int +} + +func (a *Audio) GetFrameDuration() int { + return a.Frequency * a.Frame / 1000 * a.Channels +} diff --git a/pkg/config/loader.go b/pkg/config/loader.go new file mode 100644 index 00000000..d57d2320 --- /dev/null +++ b/pkg/config/loader.go @@ -0,0 +1,25 @@ +package config + +import ( + "os" + + "github.com/kkyr/fig" +) + +// LoadConfig loads a configuration file into the given struct. +// 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) interface{} { + envPrefix := "CLOUD_GAME" + dirs := []string{path} + if path == "" { + if home, err := os.UserHomeDir(); err == nil { + dirs = append(dirs, ".", "configs", home+"/.cr", "../../../configs") + } + } + if err := fig.Load(config, fig.Dirs(dirs...), fig.UseEnv(envPrefix)); err != nil { + panic(err) + } + return config +} diff --git a/pkg/config/shared/config.go b/pkg/config/shared/config.go new file mode 100644 index 00000000..04ca2341 --- /dev/null +++ b/pkg/config/shared/config.go @@ -0,0 +1,31 @@ +package shared + +import ( + "github.com/giongto35/cloud-game/v2/pkg/environment" + flag "github.com/spf13/pflag" +) + +type Environment environment.Env + +type Server struct { + Port int + HttpsPort int + HttpsKey string + HttpsChain string +} + +func (s *Server) WithFlags() { + flag.IntVar(&s.Port, "port", s.Port, "HTTP server port") + flag.IntVar(&s.HttpsPort, "httpsPort", s.HttpsPort, "HTTPS server port (just why?)") + flag.StringVar(&s.HttpsKey, "httpsKey", s.HttpsKey, "HTTPS key") + flag.StringVar(&s.HttpsChain, "httpsChain", s.HttpsChain, "HTTPS chain") +} + +func (env *Environment) Get() environment.Env { + return (environment.Env)(*env) +} + +func (env *Environment) WithFlags() { + val := string(*env) + flag.StringVar(&val, "env", val, "Specify environment type: [dev, staging, prod]") +} diff --git a/pkg/config/webrtc/config.go b/pkg/config/webrtc/config.go new file mode 100644 index 00000000..477b2ce8 --- /dev/null +++ b/pkg/config/webrtc/config.go @@ -0,0 +1,18 @@ +package webrtc + +import "github.com/giongto35/cloud-game/v2/pkg/config/encoder" + +type Webrtc struct { + IceServers []IceServer +} + +type IceServer struct { + Url string + Username string + Credential string +} + +type Config struct { + Encoder encoder.Encoder + Webrtc Webrtc +} diff --git a/pkg/config/worker/config.go b/pkg/config/worker/config.go index b80f413a..c352954e 100644 --- a/pkg/config/worker/config.go +++ b/pkg/config/worker/config.go @@ -1,63 +1,46 @@ package worker import ( + "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/encoder" + "github.com/giongto35/cloud-game/v2/pkg/config/shared" + webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc" "github.com/giongto35/cloud-game/v2/pkg/monitoring" - "github.com/spf13/pflag" + flag "github.com/spf13/pflag" ) type Config struct { - Port int - CoordinatorAddress string - HttpPort int - - // video - Scale int - EnableAspectRatio bool - Width int - Height int - Zone string - - // WithoutGame to launch encoding with Game - WithoutGame bool - - MonitoringConfig monitoring.ServerMonitoringConfig -} - -func NewDefaultConfig() Config { - return Config{ - Port: 8800, - CoordinatorAddress: "localhost:8000", - HttpPort: 9000, - Scale: 1, - EnableAspectRatio: false, - Width: 320, - Height: 240, - WithoutGame: false, - Zone: "", - MonitoringConfig: monitoring.ServerMonitoringConfig{ - Port: 6601, - URLPrefix: "/worker", - MetricEnabled: true, - }, + Encoder encoder.Encoder + Emulator emulator.Emulator + Environment shared.Environment + Worker struct { + Monitoring monitoring.ServerMonitoringConfig + Network struct { + CoordinatorAddress string + Zone string + } + Server shared.Server } + Webrtc webrtcConfig.Webrtc } -func (c *Config) AddFlags(fs *pflag.FlagSet) *Config { - fs.IntVarP(&c.Port, "port", "", 8800, "Worker server port") - fs.StringVarP(&c.CoordinatorAddress, "coordinatorhost", "", c.CoordinatorAddress, "Worker URL to connect") - fs.IntVarP(&c.HttpPort, "httpPort", "", c.HttpPort, "Set external HTTP port") - fs.StringVarP(&c.Zone, "zone", "z", c.Zone, "Zone of the worker") +// allows custom config path +var configPath string - fs.IntVarP(&c.Scale, "scale", "s", c.Scale, "Set output viewport scale factor") - fs.BoolVarP(&c.EnableAspectRatio, "ar", "", c.EnableAspectRatio, "Enable Aspect Ratio") - fs.IntVarP(&c.Width, "width", "w", c.Width, "Set custom viewport width") - fs.IntVarP(&c.Height, "height", "h", c.Height, "Set custom viewport height") - fs.BoolVarP(&c.WithoutGame, "wogame", "", c.WithoutGame, "launch worker with game") - - fs.BoolVarP(&c.MonitoringConfig.MetricEnabled, "monitoring.metric", "m", c.MonitoringConfig.MetricEnabled, "Enable prometheus metric for server") - fs.BoolVarP(&c.MonitoringConfig.ProfilingEnabled, "monitoring.pprof", "p", c.MonitoringConfig.ProfilingEnabled, "Enable golang pprof for server") - fs.IntVarP(&c.MonitoringConfig.Port, "monitoring.port", "", c.MonitoringConfig.Port, "Monitoring server port") - fs.StringVarP(&c.MonitoringConfig.URLPrefix, "monitoring.prefix", "", c.MonitoringConfig.URLPrefix, "Monitoring server url prefix") - - return c +func NewConfig() (conf Config) { + config.LoadConfig(&conf, configPath) + return +} + +// ParseFlags updates config values from passed runtime flags. +// Define own flags with default value set to the current config param. +// Don't forget to call flag.Parse(). +func (c *Config) ParseFlags() { + c.Environment.WithFlags() + c.Worker.Server.WithFlags() + 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.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path") + flag.Parse() } diff --git a/pkg/coordinator/config.go b/pkg/coordinator/config.go deleted file mode 100644 index ce15e825..00000000 --- a/pkg/coordinator/config.go +++ /dev/null @@ -1,48 +0,0 @@ -package coordinator - -import ( - "github.com/giongto35/cloud-game/v2/pkg/monitoring" - "github.com/spf13/pflag" -) - -type Config struct { - Port int - PublicDomain string - PingServer string - URLPrefix string - DebugHost string - LibraryMonitoring bool - - MonitoringConfig monitoring.ServerMonitoringConfig -} - -func NewDefaultConfig() Config { - return Config{ - Port: 8800, - PublicDomain: "http://localhost:8000", - PingServer: "", - LibraryMonitoring: false, - - MonitoringConfig: monitoring.ServerMonitoringConfig{ - Port: 6601, - URLPrefix: "/coordinator", - MetricEnabled: false, - ProfilingEnabled: false, - }, - } -} - -func (c *Config) AddFlags(fs *pflag.FlagSet) *Config { - fs.IntVarP(&c.Port, "port", "", 8800, "Coordinator server port") - - fs.BoolVarP(&c.MonitoringConfig.MetricEnabled, "monitoring.metric", "m", c.MonitoringConfig.MetricEnabled, "Enable prometheus metric for server") - fs.BoolVarP(&c.MonitoringConfig.ProfilingEnabled, "monitoring.pprof", "p", c.MonitoringConfig.ProfilingEnabled, "Enable golang pprof for server") - fs.IntVarP(&c.MonitoringConfig.Port, "monitoring.port", "", c.MonitoringConfig.Port, "Monitoring server port") - fs.StringVarP(&c.MonitoringConfig.URLPrefix, "monitoring.prefix", "", c.MonitoringConfig.URLPrefix, "Monitoring server url prefix") - fs.StringVarP(&c.DebugHost, "debughost", "d", "", "Specify the server want to connect directly to debug") - fs.StringVarP(&c.PublicDomain, "domain", "n", c.PublicDomain, "Specify the public domain of the coordinator") - fs.StringVarP(&c.PingServer, "pingServer", "", c.PingServer, "Specify the worker address that the client can ping (with protocol and port)") - fs.BoolVarP(&c.LibraryMonitoring, "libMonitor", "", c.LibraryMonitoring, "Enable ROM library monitoring") - - return c -} diff --git a/pkg/coordinator/coordinator.go b/pkg/coordinator/coordinator.go index e53d0872..cc87b7ab 100644 --- a/pkg/coordinator/coordinator.go +++ b/pkg/coordinator/coordinator.go @@ -6,9 +6,11 @@ import ( "fmt" "log" "net/http" + "strconv" "time" - "github.com/giongto35/cloud-game/v2/pkg/config" + "github.com/giongto35/cloud-game/v2/pkg/config/coordinator" + "github.com/giongto35/cloud-game/v2/pkg/environment" "github.com/giongto35/cloud-game/v2/pkg/games" "github.com/giongto35/cloud-game/v2/pkg/monitoring" "github.com/golang/glog" @@ -21,17 +23,17 @@ const stagingLEURL = "https://acme-staging-v02.api.letsencrypt.org/directory" type Coordinator struct { ctx context.Context - cfg Config + cfg coordinator.Config monitoringServer *monitoring.ServerMonitoring } -func New(ctx context.Context, cfg Config) *Coordinator { +func New(ctx context.Context, cfg coordinator.Config) *Coordinator { return &Coordinator{ ctx: ctx, cfg: cfg, - monitoringServer: monitoring.NewServerMonitoring(cfg.MonitoringConfig), + monitoringServer: monitoring.NewServerMonitoring(cfg.Coordinator.Monitoring), } } @@ -96,31 +98,31 @@ func makeHTTPToHTTPSRedirectServer(server *Server) *http.Server { // initializeCoordinator setup an coordinator server func (o *Coordinator) initializeCoordinator() { // init games library - lib := games.NewLibrary(games.Config{ - BasePath: "assets/games", - Supported: config.SupportedRomExtensions, - Ignored: []string{"neogeo", "pgm"}, - Verbose: true, - WatchMode: o.cfg.LibraryMonitoring, - }) + libraryConf := o.cfg.Coordinator.Library + if len(libraryConf.Supported) == 0 { + libraryConf.Supported = o.cfg.Emulator.GetSupportedExtensions() + } + lib := games.NewLibrary(libraryConf) lib.Scan() - coordinator := NewServer(o.cfg, lib) + server := NewServer(o.cfg, lib) var certManager *autocert.Manager var httpsSrv *http.Server log.Println("Initializing Coordinator Server") - if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv { - httpsSrv = makeHTTPServer(coordinator) - httpsSrv.Addr = fmt.Sprintf(":%d", *config.HttpsPort) + mode := o.cfg.Environment.Get() + if mode.AnyOf(environment.Production, environment.Staging) { + serverConfig := o.cfg.Coordinator.Server + httpsSrv = makeHTTPServer(server) + httpsSrv.Addr = fmt.Sprintf(":%d", serverConfig.HttpsPort) - if *config.HttpsChain == "" || *config.HttpsKey == "" { - *config.HttpsChain = "" - *config.HttpsKey = "" + if serverConfig.HttpsChain == "" || serverConfig.HttpsKey == "" { + serverConfig.HttpsChain = "" + serverConfig.HttpsKey = "" var leurl string - if *config.Mode == config.StagingEnv { + if mode == environment.Staging { leurl = stagingLEURL } else { leurl = acme.LetsEncryptURL @@ -128,7 +130,7 @@ func (o *Coordinator) initializeCoordinator() { certManager = &autocert.Manager{ Prompt: autocert.AcceptTOS, - HostPolicy: autocert.HostWhitelist(o.cfg.PublicDomain), + HostPolicy: autocert.HostWhitelist(o.cfg.Coordinator.PublicDomain), Cache: autocert.DirCache("assets/cache"), Client: &acme.Client{DirectoryURL: leurl}, } @@ -136,27 +138,27 @@ func (o *Coordinator) initializeCoordinator() { httpsSrv.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate} } - go func() { + go func(chain string, key string) { fmt.Printf("Starting HTTPS server on %s\n", httpsSrv.Addr) - err := httpsSrv.ListenAndServeTLS(*config.HttpsChain, *config.HttpsKey) + err := httpsSrv.ListenAndServeTLS(chain, key) if err != nil { log.Fatalf("httpsSrv.ListendAndServeTLS() failed with %s", err) } - }() + }(serverConfig.HttpsChain, serverConfig.HttpsKey) } var httpSrv *http.Server - if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv { - httpSrv = makeHTTPToHTTPSRedirectServer(coordinator) + if mode.AnyOf(environment.Production, environment.Staging) { + httpSrv = makeHTTPToHTTPSRedirectServer(server) } else { - httpSrv = makeHTTPServer(coordinator) + httpSrv = makeHTTPServer(server) } if certManager != nil { httpSrv.Handler = certManager.HTTPHandler(httpSrv.Handler) } - httpSrv.Addr = ":" + *config.HttpPort + httpSrv.Addr = ":" + strconv.Itoa(o.cfg.Coordinator.Server.Port) err := httpSrv.ListenAndServe() if err != nil { log.Fatalf("httpSrv.ListenAndServe() failed with %s", err) diff --git a/pkg/coordinator/handlers.go b/pkg/coordinator/handlers.go index 3b0b6d96..636d7742 100644 --- a/pkg/coordinator/handlers.go +++ b/pkg/coordinator/handlers.go @@ -10,20 +10,20 @@ import ( "net/http" "strings" - "github.com/giongto35/cloud-game/v2/pkg/config" + "github.com/giongto35/cloud-game/v2/pkg/config/coordinator" "github.com/giongto35/cloud-game/v2/pkg/cws" + "github.com/giongto35/cloud-game/v2/pkg/environment" "github.com/giongto35/cloud-game/v2/pkg/games" "github.com/giongto35/cloud-game/v2/pkg/util" + "github.com/giongto35/cloud-game/v2/pkg/webrtc" "github.com/gofrs/uuid" "github.com/gorilla/websocket" ) -const ( - gameboyIndex = "./web/game.html" -) +const index = "./web/index.html" type Server struct { - cfg Config + cfg coordinator.Config // games library library games.GameLibrary // roomToWorker map roomID to workerID @@ -40,7 +40,7 @@ const devPingServer = "http://localhost:9000/echo" var upgrader = websocket.Upgrader{} var errNotFound = errors.New("Not found") -func NewServer(cfg Config, library games.GameLibrary) *Server { +func NewServer(cfg coordinator.Config, library games.GameLibrary) *Server { return &Server{ cfg: cfg, library: library, @@ -53,36 +53,25 @@ func NewServer(cfg Config, library games.GameLibrary) *Server { } } -type RenderData struct { - STUNTURN string -} - // GetWeb returns web frontend func (o *Server) GetWeb(w http.ResponseWriter, r *http.Request) { - stunturn := *config.FrontendSTUNTURN - if stunturn == "" { - stunturn = config.DefaultSTUNTURN - } - data := RenderData{ - STUNTURN: stunturn, - } - - tmpl, err := template.ParseFiles(gameboyIndex) + tmpl, err := template.ParseFiles(index) if err != nil { log.Fatal(err) } - tmpl.Execute(w, data) + tmpl.Execute(w, struct{}{}) } // getPingServer returns the server for latency check of a zone. In latency check to find best worker step, we use this server to find the closest worker. func (o *Server) getPingServer(zone string) string { - if o.cfg.PingServer != "" { - return fmt.Sprintf("%s/echo", o.cfg.PingServer) + if o.cfg.Coordinator.PingServer != "" { + return fmt.Sprintf("%s/echo", o.cfg.Coordinator.PingServer) } - if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv { - return fmt.Sprintf(pingServerTemp, zone, o.cfg.PublicDomain) + mode := o.cfg.Environment.Get() + if mode.AnyOf(environment.Production, environment.Staging) { + return fmt.Sprintf(pingServerTemp, zone, o.cfg.Coordinator.PublicDomain) } // If not Prod or Staging, return dev environment @@ -127,7 +116,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { wc.Printf("Set ping server address: %s", pingServer) // In case worker and coordinator in the same host - if !util.IsPublicIP(address) && *config.Mode == config.ProdEnv { + if !util.IsPublicIP(address) && o.cfg.Environment.Get() == environment.Production { // Don't accept private IP for worker's address in prod mode // However, if the worker in the same host with coordinator, we can get public IP of worker wc.Printf("[!] Address %s is invalid", address) @@ -144,7 +133,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { // Create a workerClient instance wc.Address = address - wc.StunTurnServer = fmt.Sprintf(config.StunTurnTemplate, address, address) + wc.StunTurnServer = webrtc.ToJson(o.cfg.Webrtc.IceServers, webrtc.Replacement{From: "server-ip", To: address}) wc.Zone = zone wc.PingServer = pingServer @@ -265,9 +254,10 @@ func (o *Server) WS(w http.ResponseWriter, r *http.Request) { } func (o *Server) getBestWorkerClient(client *BrowserClient, zone string) (*WorkerClient, error) { - if o.cfg.DebugHost != "" { - client.Println("Connecting to debug host instead prod servers", o.cfg.DebugHost) - wc := o.getWorkerFromAddress(o.cfg.DebugHost) + conf := o.cfg.Coordinator + if conf.DebugHost != "" { + client.Println("Connecting to debug host instead prod servers", conf.DebugHost) + wc := o.getWorkerFromAddress(conf.DebugHost) if wc != nil { return wc, nil } diff --git a/pkg/cws/cws.go b/pkg/cws/cws.go index d368b6c5..97cc29e0 100644 --- a/pkg/cws/cws.go +++ b/pkg/cws/cws.go @@ -7,7 +7,6 @@ import ( "sync" "time" - "github.com/giongto35/cloud-game/v2/pkg/config" "github.com/gofrs/uuid" "github.com/gorilla/websocket" ) @@ -42,6 +41,8 @@ type WSPacket struct { var EmptyPacket = WSPacket{} +const WSWait = 20 * time.Second + func NewClient(conn *websocket.Conn) *Client { id := uuid.Must(uuid.NewV4()).String() sendCallback := map[string]func(WSPacket){} @@ -86,7 +87,7 @@ func (c *Client) Send(request WSPacket, callback func(response WSPacket)) { } c.sendLock.Lock() - c.conn.SetWriteDeadline(time.Now().Add(config.WSWait)) + c.conn.SetWriteDeadline(time.Now().Add(WSWait)) c.conn.WriteMessage(websocket.TextMessage, data) c.sendLock.Unlock() } @@ -115,7 +116,7 @@ func (c *Client) Receive(id string, f func(response WSPacket) (request WSPacket) log.Println("[!] json marshal error:", err) } c.sendLock.Lock() - c.conn.SetWriteDeadline(time.Now().Add(config.WSWait)) + c.conn.SetWriteDeadline(time.Now().Add(WSWait)) c.conn.WriteMessage(websocket.TextMessage, resp) c.sendLock.Unlock() } @@ -166,7 +167,7 @@ func (c *Client) Heartbeat() { func (c *Client) Listen() { for { - c.conn.SetReadDeadline(time.Now().Add(config.WSWait)) + c.conn.SetReadDeadline(time.Now().Add(WSWait)) _, rawMsg, err := c.conn.ReadMessage() if err != nil { log.Println("[!] read:", err) diff --git a/pkg/downloader/backend/grab.go b/pkg/downloader/backend/grab.go new file mode 100644 index 00000000..32f6178c --- /dev/null +++ b/pkg/downloader/backend/grab.go @@ -0,0 +1,42 @@ +package backend + +import ( + "log" + + "github.com/cavaliercoder/grab" +) + +type GrabDownloader struct { + client *grab.Client + concurrency int +} + +func NewGrabDownloader() GrabDownloader { + return GrabDownloader{ + client: grab.NewClient(), + concurrency: 5, + } +} + +func (d GrabDownloader) Request(dest string, urls ...string) (files []string) { + reqs := make([]*grab.Request, 0) + for _, url := range urls { + req, err := grab.NewRequest(dest, url) + if err != nil { + log.Printf("error: couldn't make request URL: %v, %v", url, err) + } else { + reqs = append(reqs, req) + } + } + + // check each response + for resp := range d.client.DoBatch(d.concurrency, reqs...) { + if err := resp.Err(); err != nil { + log.Printf("error: download failed: %v\n", err) + } else { + log.Printf("Downloaded [%v] %s\n", resp.HTTPResponse.Status, resp.Filename) + files = append(files, resp.Filename) + } + } + return +} diff --git a/pkg/downloader/downloader.go b/pkg/downloader/downloader.go new file mode 100644 index 00000000..42e32dd2 --- /dev/null +++ b/pkg/downloader/downloader.go @@ -0,0 +1,42 @@ +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 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 client interface { + Request(dest string, urls ...string) []string +} + +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 ...string) []string { + files := d.backend.Request(dest, urls...) + for _, op := range d.pipe { + files = op(dest, files) + } + return files +} diff --git a/pkg/downloader/pipe/pipe.go b/pkg/downloader/pipe/pipe.go new file mode 100644 index 00000000..14be06fd --- /dev/null +++ b/pkg/downloader/pipe/pipe.go @@ -0,0 +1,29 @@ +package pipe + +import ( + "os" + + "github.com/giongto35/cloud-game/v2/pkg/extractor" +) + +func Unpack(dest string, files []string) []string { + var res []string + for _, file := range files { + if unpack := extractor.NewFromExt(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 +} diff --git a/pkg/emulator/emulator.go b/pkg/emulator/emulator.go index daa2a289..f9fe0a79 100644 --- a/pkg/emulator/emulator.go +++ b/pkg/emulator/emulator.go @@ -1,11 +1,11 @@ package emulator -import "github.com/giongto35/cloud-game/v2/pkg/config" +import "github.com/giongto35/cloud-game/v2/pkg/emulator/image" // CloudEmulator is the interface of cloud emulator. Currently NES emulator and RetroArch implements this in codebase type CloudEmulator interface { // LoadMeta returns meta data of emulator. Refer below - LoadMeta(path string) config.EmulatorMeta + LoadMeta(path string) Metadata // Start is called after LoadGame Start() // SetViewport sets viewport size @@ -23,3 +23,21 @@ type CloudEmulator interface { 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 +} diff --git a/pkg/emulator/libretro/core/core.go b/pkg/emulator/libretro/core/core.go new file mode 100644 index 00000000..02c9ad02 --- /dev/null +++ b/pkg/emulator/libretro/core/core.go @@ -0,0 +1,38 @@ +package core + +import ( + "errors" + "runtime" +) + +// See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63. +var libretroOsArchMap = map[string]ArchInfo{ + "linux:amd64": {Os: "linux", Arch: "x86_64", LibExt: ".so"}, + "linux:arm": {Os: "linux", Arch: "armv7-neon-hf", LibExt: ".armv7-neon-hf.so"}, + "windows:amd64": {Os: "windows", Arch: "x86_64", LibExt: ".dll"}, + "darwin:amd64": {Os: "osx", Arch: "x86_64", Vendor: "apple", LibExt: ".dylib"}, +} + +// ArchInfo contains Libretro core lib platform info. +// And cores are just C-compiled libraries. +// See: https://buildbot.libretro.com/nightly. +type ArchInfo struct { + // bottom: x86_64, x86, ... + Arch string + // middle: windows, ios, ... + Os string + // top level: apple, nintendo, ... + Vendor string + + // platform dependent library file extension (dot-prefixed) + LibExt string +} + +func GetCoreExt() (ArchInfo, error) { + key := runtime.GOOS + ":" + runtime.GOARCH + if arch, ok := libretroOsArchMap[key]; ok { + return arch, nil + } else { + return ArchInfo{}, errors.New("core mapping not found for " + key) + } +} diff --git a/pkg/emulator/libretro/manager/manager.go b/pkg/emulator/libretro/manager/manager.go new file mode 100644 index 00000000..3a89ae8b --- /dev/null +++ b/pkg/emulator/libretro/manager/manager.go @@ -0,0 +1,42 @@ +package manager + +import ( + "io/ioutil" + "log" + "path/filepath" + "strings" + + "github.com/giongto35/cloud-game/v2/pkg/config/emulator" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" +) + +type Manager interface { + Sync() error +} + +type BasicManager struct { + Conf emulator.LibretroConfig +} + +func (m BasicManager) GetInstalled() (installed []string) { + dir := m.Conf.GetCoresStorePath() + arch, err := core.GetCoreExt() + if err != nil { + log.Printf("error: %v", err) + return + } + + files, err := ioutil.ReadDir(dir) + if err != nil { + log.Printf("error: couldn't get installed cores, %v", err) + return + } + + for _, file := range files { + name := file.Name() + if filepath.Ext(name) == arch.LibExt { + installed = append(installed, strings.TrimSuffix(name, arch.LibExt)) + } + } + return +} diff --git a/pkg/emulator/libretro/manager/remotehttp/manager.go b/pkg/emulator/libretro/manager/remotehttp/manager.go new file mode 100644 index 00000000..5d98093d --- /dev/null +++ b/pkg/emulator/libretro/manager/remotehttp/manager.go @@ -0,0 +1,96 @@ +package remotehttp + +import ( + "log" + "os" + "strings" + + "github.com/giongto35/cloud-game/v2/pkg/config/emulator" + "github.com/giongto35/cloud-game/v2/pkg/downloader" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/manager" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo/buildbot" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo/github" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo/raw" + "github.com/gofrs/flock" +) + +type Manager struct { + manager.BasicManager + + repo repo.Repository + client downloader.Downloader + fmu *flock.Flock +} + +func NewRemoteHttpManager(conf emulator.LibretroConfig) Manager { + repoConf := conf.Cores.Repo + + var repository repo.Repository + switch repoConf.Type { + case "raw": + repository = raw.NewRawRepo(repoConf.Url) + case "github": + repository = github.NewGithubRepo(repoConf.Url, repoConf.Compression) + case "buildbot": + fallthrough + default: + repository = buildbot.NewBuildbotRepo(repoConf.Url, repoConf.Compression) + } + + // used for synchronization of multiple process + fileLock := os.TempDir() + string(os.PathSeparator) + "cloud_game.lock" + + return Manager{ + BasicManager: manager.BasicManager{ + Conf: conf, + }, + repo: repository, + client: downloader.NewDefaultDownloader(), + fmu: flock.New(fileLock), + } +} + +func (m Manager) Sync() error { + declared := m.Conf.GetCores() + dir := m.Conf.GetCoresStorePath() + + // IPC lock if multiple worker processes on the same machine + m.fmu.Lock() + defer m.fmu.Unlock() + + installed := m.GetInstalled() + download := diff(installed, declared) + + if len(download) > 0 { + log.Printf("Starting Libretro cores download: %v", strings.Join(download, ", ")) + m.client.Download(dir, m.getCoreUrls(download)...) + } + return nil +} + +func (m Manager) getCoreUrls(names []string) (urls []string) { + arch, err := core.GetCoreExt() + if err != nil { + return + } + for _, c := range names { + urls = append(urls, m.repo.GetCoreData(c, arch).Url) + } + return +} + +// diff returns a list of not installed cores. +func diff(declared, installed []string) (diff []string) { + v := map[string]struct{}{} + for _, x := range declared { + v[x] = struct{}{} + } + for _, x := range installed { + if _, ok := v[x]; !ok { + diff = append(diff, x) + } + } + return +} diff --git a/pkg/emulator/libretro/manager/remotehttp/manager_test.go b/pkg/emulator/libretro/manager/remotehttp/manager_test.go new file mode 100644 index 00000000..616798c8 --- /dev/null +++ b/pkg/emulator/libretro/manager/remotehttp/manager_test.go @@ -0,0 +1,37 @@ +package remotehttp + +import ( + "reflect" + "testing" +) + +func TestDiff(t *testing.T) { + tests := []struct { + declared []string + installed []string + diff []string + }{ + {}, + { + installed: []string{"c"}, + }, + { + declared: []string{"a", "b", "c"}, + installed: []string{"c"}, + diff: []string{"a", "b"}, + }, + { + declared: []string{"a", "b", "c", "c", "c", "a", "d"}, + installed: []string{"c", "c", "c", "a", "a", "a"}, + diff: []string{"b", "d"}, + }, + } + + for _, test := range tests { + difference := diff(test.declared, test.installed) + if !reflect.DeepEqual(test.diff, difference) { + t.Errorf("wrong diff for %v <- %v = %v != %v", + test.declared, test.installed, test.diff, difference) + } + } +} diff --git a/pkg/emulator/libretro/nanoarch/loader.go b/pkg/emulator/libretro/nanoarch/loader.go new file mode 100644 index 00000000..f5f0e61c --- /dev/null +++ b/pkg/emulator/libretro/nanoarch/loader.go @@ -0,0 +1,73 @@ +package nanoarch + +import ( + "errors" + "io/ioutil" + "path" + "strconv" + "strings" + "unsafe" +) + +/* +#cgo LDFLAGS: -ldl +#include +#include +*/ +import "C" + +func open(file string) unsafe.Pointer { + cs := C.CString(file) + defer C.free(unsafe.Pointer(cs)) + return C.dlopen(cs, C.RTLD_LAZY) +} + +func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer { + cs := C.CString(name) + defer C.free(unsafe.Pointer(cs)) + pointer := C.dlsym(handle, cs) + return pointer +} + +func loadLib(filepath string) (handle unsafe.Pointer, err error) { + handle = open(filepath) + if handle == nil { + e := C.dlerror() + if e != nil { + err = errors.New(C.GoString(e)) + } else { + err = errors.New("couldn't load the lib") + } + } + return +} + +func loadLibRollingRollingRolling(filepath string) (handle unsafe.Pointer, err error) { + dir, lib := path.Dir(filepath), path.Base(filepath) + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, errors.New("couldn't find 'n load the lib") + } + + for _, file := range files { + if !file.IsDir() && strings.HasPrefix(file.Name(), lib) { + handle = open(path.Join(dir, file.Name())) + if handle != nil { + return handle, nil + } + } + } + return nil, errors.New("couldn't find 'n load the lib") +} + +func closeLib(handle unsafe.Pointer) (err error) { + if handle == nil { + return + } + + code := int(C.dlclose(handle)) + if code != 0 { + return errors.New("couldn't close the lib (" + strconv.Itoa(code) + ")") + } + return +} diff --git a/pkg/emulator/libretro/nanoarch/naemulator.go b/pkg/emulator/libretro/nanoarch/naemulator.go index d67c8c35..f61d2464 100644 --- a/pkg/emulator/libretro/nanoarch/naemulator.go +++ b/pkg/emulator/libretro/nanoarch/naemulator.go @@ -10,7 +10,8 @@ import ( "sync" "time" - "github.com/giongto35/cloud-game/v2/pkg/config" + config "github.com/giongto35/cloud-game/v2/pkg/config/emulator" + "github.com/giongto35/cloud-game/v2/pkg/emulator" "github.com/giongto35/cloud-game/v2/pkg/util" ) @@ -64,7 +65,7 @@ type naEmulator struct { inputChannel <-chan InputEvent videoExporter *VideoExporter - meta config.EmulatorMeta + meta emulator.Metadata gamePath string roomID string gameName string @@ -103,13 +104,20 @@ const maxPort = 8 const SocketAddrTmpl = "/tmp/cloudretro-retro-%s.sock" // NAEmulator implements CloudEmulator interface based on NanoArch(golang RetroArch) -func NewNAEmulator(etype string, roomID string, inputChannel <-chan InputEvent) (*naEmulator, chan GameFrame, chan []int16) { - meta := config.EmulatorConfig[etype] +func NewNAEmulator(roomID string, inputChannel <-chan InputEvent, conf config.LibretroCoreConfig) (*naEmulator, chan GameFrame, chan []int16) { imageChannel := make(chan GameFrame, 30) audioChannel := make(chan []int16, 30) return &naEmulator{ - meta: meta, + meta: emulator.Metadata{ + LibPath: conf.Lib, + ConfigPath: conf.Config, + Ratio: conf.Ratio, + IsGlAllowed: conf.IsGlAllowed, + UsesLibCo: conf.UsesLibCo, + HasMultitap: conf.HasMultitap, + AutoGlContext: conf.AutoGlContext, + }, imageChannel: imageChannel, audioChannel: audioChannel, inputChannel: inputChannel, @@ -152,8 +160,8 @@ func NewVideoExporter(roomID string, imgChannel chan GameFrame) *VideoExporter { // Init initialize new RetroArch cloud emulator // withImageChan returns an image stream as Channel for output else it will write to unix socket -func Init(etype string, roomID string, withImageChannel bool, inputChannel <-chan InputEvent) (*naEmulator, chan GameFrame, chan []int16) { - emulator, imageChannel, audioChannel := NewNAEmulator(etype, roomID, inputChannel) +func Init(roomID string, withImageChannel bool, inputChannel <-chan InputEvent, config config.LibretroCoreConfig) (*naEmulator, chan GameFrame, chan []int16) { + emulator, imageChannel, audioChannel := NewNAEmulator(roomID, inputChannel, config) // Set to global NAEmulator NAEmulator = emulator if !withImageChannel { @@ -188,7 +196,7 @@ func (na *naEmulator) listenInput() { } } -func (na *naEmulator) LoadMeta(path string) config.EmulatorMeta { +func (na *naEmulator) LoadMeta(path string) emulator.Metadata { coreLoad(na.meta) coreLoadGame(path) na.gamePath = path diff --git a/pkg/emulator/libretro/nanoarch/nanoarch.go b/pkg/emulator/libretro/nanoarch/nanoarch.go index 6b44a552..ff21c2c3 100644 --- a/pkg/emulator/libretro/nanoarch/nanoarch.go +++ b/pkg/emulator/libretro/nanoarch/nanoarch.go @@ -12,18 +12,17 @@ import ( "time" "unsafe" - "github.com/giongto35/cloud-game/v2/pkg/config" + "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" -#cgo LDFLAGS: -ldl #include #include -#include #include void bridge_retro_init(void *f); @@ -413,39 +412,31 @@ var retroSerialize unsafe.Pointer var retroUnserialize unsafe.Pointer var retroSetControllerPortDevice unsafe.Pointer -func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer { - cs := C.CString(name) - pointer := C.dlsym(handle, cs) - C.free(unsafe.Pointer(cs)) - return pointer -} - -func coreLoad(meta config.EmulatorMeta) { +func coreLoad(meta emulator.Metadata) { isGlAllowed = meta.IsGlAllowed usesLibCo = meta.UsesLibCo video.autoGlContext = meta.AutoGlContext - coreConfig = ScanConfigFile(meta.Config) + coreConfig = ScanConfigFile(meta.ConfigPath) multitap.supported = meta.HasMultitap multitap.enabled = false multitap.value = 0 - mu.Lock() - // Different OS requires different library, bruteforce till it finish - for _, ext := range config.EmulatorExtension { - pathWithExt := meta.Path + ext - cs := C.CString(pathWithExt) - retroHandle = C.dlopen(cs, C.RTLD_LAZY) - C.free(unsafe.Pointer(cs)) - if retroHandle != nil { - break - } + filePath := meta.LibPath + if arch, err := core.GetCoreExt(); err == nil { + filePath = filePath + arch.LibExt + } else { + log.Printf("warning: %v", err) } - if retroHandle == nil { - err := C.dlerror() + 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", meta.Path, C.GoString(err)) + log.Fatalf("error core load: %s, %v", filePath, err) } } @@ -669,8 +660,8 @@ func nanoarchShutdown() { } setRotation(0) - if r := C.dlclose(retroHandle); r != 0 { - log.Printf("couldn't close the core") + if err := closeLib(retroHandle); err != nil { + log.Printf("error when close: %v", err) } for _, element := range coreConfig { C.free(unsafe.Pointer(element)) diff --git a/pkg/emulator/libretro/nanoarch/nanoarch_test.go b/pkg/emulator/libretro/nanoarch/nanoarch_test.go index 1175661d..2cc20a58 100644 --- a/pkg/emulator/libretro/nanoarch/nanoarch_test.go +++ b/pkg/emulator/libretro/nanoarch/nanoarch_test.go @@ -9,12 +9,12 @@ import ( "os" "path" "path/filepath" - "runtime" - "strings" "sync" "testing" "github.com/giongto35/cloud-game/v2/pkg/config" + "github.com/giongto35/cloud-game/v2/pkg/config/worker" + "github.com/giongto35/cloud-game/v2/pkg/emulator" ) type testRun struct { @@ -57,8 +57,13 @@ type EmulatorPaths struct { // Don't forget to init one image channel consumer, it will lock-out otherwise. // Make sure you call shutdownEmulator(). func GetEmulatorMock(room string, system string) *EmulatorMock { - assetsPath := getAssetsPath() - metadata := config.EmulatorConfig[system] + rootPath := getRootPath() + configPath := rootPath + "configs/" + + var conf worker.Config + config.LoadConfig(&conf, configPath) + + meta := conf.Emulator.GetLibretroCoreConfig(system) images := make(chan GameFrame, 30) audio := make(chan []int16, 30) @@ -71,20 +76,27 @@ func GetEmulatorMock(room string, system string) *EmulatorMock { audioChannel: audio, inputChannel: inputs, - meta: metadata, + meta: emulator.Metadata{ + LibPath: meta.Lib, + ConfigPath: meta.Config, + Ratio: meta.Ratio, + IsGlAllowed: meta.IsGlAllowed, + UsesLibCo: meta.UsesLibCo, + HasMultitap: meta.HasMultitap, + }, controllersMap: map[string][]controllerState{}, roomID: room, done: make(chan struct{}, 1), lock: &sync.Mutex{}, }, - canvas: image.NewRGBA(image.Rect(0, 0, metadata.Width, metadata.Height)), - core: path.Base(metadata.Path), + canvas: image.NewRGBA(image.Rect(0, 0, meta.Width, meta.Height)), + core: path.Base(meta.Lib), paths: EmulatorPaths{ - assets: cleanPath(assetsPath), - cores: cleanPath(assetsPath + "emulator/libretro/cores/"), - games: cleanPath(assetsPath + "games/"), + assets: cleanPath(rootPath), + cores: cleanPath(rootPath + "assets/cores/"), + games: cleanPath(rootPath + "assets/games/"), }, imageInCh: images, @@ -117,8 +129,8 @@ func GetDefaultEmulatorMock(room string, system string, rom string) *EmulatorMoc // The rom will be loaded from emulators' games path. func (emu *EmulatorMock) loadRom(game string) { fmt.Printf("%v %v\n", emu.paths.cores, emu.core) - coreLoad(config.EmulatorMeta{ - Path: emu.paths.cores + emu.core, + coreLoad(emulator.Metadata{ + LibPath: emu.paths.cores + emu.core, }) coreLoadGame(emu.paths.games + game) } @@ -192,12 +204,10 @@ func (emu *EmulatorMock) getStateHash() string { return getHash(state) } -// getAssetsPath returns absolute path to the assets directory. -func getAssetsPath() string { - appName := "cloud-game" - // get app path at runtime - _, b, _, _ := runtime.Caller(0) - return filepath.Dir(strings.SplitAfter(b, appName)[0]) + "/" + appName + "/assets/" +// getRootPath returns absolute path to the root directory. +func getRootPath() string { + p, _ := filepath.Abs("../../../../") + return p + string(filepath.Separator) } // getHash returns MD5 hash. diff --git a/pkg/emulator/libretro/repo/buildbot/repository.go b/pkg/emulator/libretro/repo/buildbot/repository.go new file mode 100644 index 00000000..28d45690 --- /dev/null +++ b/pkg/emulator/libretro/repo/buildbot/repository.go @@ -0,0 +1,30 @@ +package buildbot + +import ( + "strings" + + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo" +) + +type Repo struct { + address string + compression repo.CompressionType +} + +func NewBuildbotRepo(address string, compression string) Repo { + return Repo{address: address, compression: (repo.CompressionType)(compression)} +} + +func (r Repo) GetCoreData(file string, info core.ArchInfo) repo.Data { + var sb strings.Builder + sb.WriteString(r.address + "/") + if info.Vendor != "" { + sb.WriteString(info.Vendor + "/") + } + sb.WriteString(info.Os + "/" + info.Arch + "/latest/" + file + info.LibExt) + if r.compression != "" { + sb.WriteString("." + r.compression.GetExt()) + } + return repo.Data{Url: sb.String(), Compression: r.compression} +} diff --git a/pkg/emulator/libretro/repo/buildbot/repository_test.go b/pkg/emulator/libretro/repo/buildbot/repository_test.go new file mode 100644 index 00000000..f129bdaf --- /dev/null +++ b/pkg/emulator/libretro/repo/buildbot/repository_test.go @@ -0,0 +1,56 @@ +package buildbot + +import ( + "testing" + + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" +) + +func TestBuildbotRepo(t *testing.T) { + testAddress := "http://test.me" + tests := []struct { + file string + compression string + arch core.ArchInfo + resultUrl string + }{ + { + file: "uber_core", + arch: core.ArchInfo{ + Os: "linux", + Arch: "x86_64", + LibExt: ".so", + }, + resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so", + }, + { + file: "uber_core", + compression: "zip", + arch: core.ArchInfo{ + Os: "linux", + Arch: "x86_64", + LibExt: ".so", + }, + resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip", + }, + { + file: "uber_core", + arch: core.ArchInfo{ + Os: "osx", + Arch: "x86_64", + Vendor: "apple", + LibExt: ".dylib", + }, + resultUrl: testAddress + "/" + "apple/osx/x86_64/latest/uber_core.dylib", + }, + } + + for _, test := range tests { + repo := NewBuildbotRepo(testAddress, test.compression) + data := repo.GetCoreData(test.file, test.arch) + if data.Url != test.resultUrl { + t.Errorf("seems that expected link address is incorrect (%v) for file %s %+v", + data.Url, test.file, test.arch) + } + } +} diff --git a/pkg/emulator/libretro/repo/compression.go b/pkg/emulator/libretro/repo/compression.go new file mode 100644 index 00000000..15804fa2 --- /dev/null +++ b/pkg/emulator/libretro/repo/compression.go @@ -0,0 +1,7 @@ +package repo + +type CompressionType string + +func (c *CompressionType) GetExt() string { + return (string)(*c) +} diff --git a/pkg/emulator/libretro/repo/github/repository.go b/pkg/emulator/libretro/repo/github/repository.go new file mode 100644 index 00000000..48787e68 --- /dev/null +++ b/pkg/emulator/libretro/repo/github/repository.go @@ -0,0 +1,20 @@ +package github + +import ( + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo/buildbot" +) + +type Repo struct { + buildbot.Repo +} + +func NewGithubRepo(address string, compression string) Repo { + return Repo{Repo: buildbot.NewBuildbotRepo(address, compression)} +} + +func (r Repo) GetCoreData(file string, info core.ArchInfo) repo.Data { + dat := r.Repo.GetCoreData(file, info) + return repo.Data{Url: dat.Url + "?raw=true", Compression: dat.Compression} +} diff --git a/pkg/emulator/libretro/repo/github/repository_test.go b/pkg/emulator/libretro/repo/github/repository_test.go new file mode 100644 index 00000000..0b52a0a0 --- /dev/null +++ b/pkg/emulator/libretro/repo/github/repository_test.go @@ -0,0 +1,56 @@ +package github + +import ( + "testing" + + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" +) + +func TestBuildbotRepo(t *testing.T) { + testAddress := "http://test.me" + tests := []struct { + file string + compression string + arch core.ArchInfo + resultUrl string + }{ + { + file: "uber_core", + arch: core.ArchInfo{ + Os: "linux", + Arch: "x86_64", + LibExt: ".so", + }, + resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so?raw=true", + }, + { + file: "uber_core", + compression: "zip", + arch: core.ArchInfo{ + Os: "linux", + Arch: "x86_64", + LibExt: ".so", + }, + resultUrl: testAddress + "/" + "linux/x86_64/latest/uber_core.so.zip?raw=true", + }, + { + file: "uber_core", + arch: core.ArchInfo{ + Os: "osx", + Arch: "x86_64", + Vendor: "apple", + LibExt: ".dylib", + }, + resultUrl: testAddress + "/" + "apple/osx/x86_64/latest/uber_core.dylib?raw=true", + }, + } + + for _, test := range tests { + repo := NewGithubRepo(testAddress, test.compression) + data := repo.GetCoreData(test.file, test.arch) + if data.Url != test.resultUrl { + t.Errorf("seems that expected link address is incorrect (%v) for file %s %+v", + data.Url, test.file, test.arch) + } + } +} diff --git a/pkg/emulator/libretro/repo/raw/repository.go b/pkg/emulator/libretro/repo/raw/repository.go new file mode 100644 index 00000000..344e2eb8 --- /dev/null +++ b/pkg/emulator/libretro/repo/raw/repository.go @@ -0,0 +1,24 @@ +package raw + +import ( + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/repo" +) + +type Repo struct { + address string + compression repo.CompressionType +} + +// NewRawRepo defines a simple zip file containing +// all the cores that will be extracted as is. +func NewRawRepo(address string) Repo { + return Repo{ + address: address, + compression: "zip", + } +} + +func (r Repo) GetCoreData(_ string, _ core.ArchInfo) repo.Data { + return repo.Data{Url: r.address, Compression: r.compression} +} diff --git a/pkg/emulator/libretro/repo/repository.go b/pkg/emulator/libretro/repo/repository.go new file mode 100644 index 00000000..d412dd2a --- /dev/null +++ b/pkg/emulator/libretro/repo/repository.go @@ -0,0 +1,14 @@ +package repo + +import "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/core" + +type ( + Data struct { + Url string + Compression CompressionType + } + + Repository interface { + GetCoreData(file string, info core.ArchInfo) Data + } +) diff --git a/pkg/encoder/codec.go b/pkg/encoder/codec.go new file mode 100644 index 00000000..1120a265 --- /dev/null +++ b/pkg/encoder/codec.go @@ -0,0 +1,8 @@ +package encoder + +type VideoCodec int + +const ( + H264 VideoCodec = iota + VPX +) diff --git a/pkg/environment/env.go b/pkg/environment/env.go new file mode 100644 index 00000000..106fb74f --- /dev/null +++ b/pkg/environment/env.go @@ -0,0 +1,18 @@ +package environment + +type Env string + +const ( + Dev Env = "dev" + Staging = "staging" + Production = "prod" +) + +func (env *Env) AnyOf(what ...Env) bool { + for _, cur := range what { + if *env == cur { + return true + } + } + return false +} diff --git a/pkg/extractor/extractor.go b/pkg/extractor/extractor.go new file mode 100644 index 00000000..686d776f --- /dev/null +++ b/pkg/extractor/extractor.go @@ -0,0 +1,24 @@ +package extractor + +import ( + "path/filepath" + + "github.com/giongto35/cloud-game/v2/pkg/extractor/zip" +) + +type Extractor interface { + Extract(src string, dest string) ([]string, error) +} + +const ( + zipExt = ".zip" +) + +func NewFromExt(path string) Extractor { + switch filepath.Ext(path) { + case zipExt: + return zip.New() + default: + return nil + } +} diff --git a/pkg/extractor/zip/extractor.go b/pkg/extractor/zip/extractor.go new file mode 100644 index 00000000..f9fee462 --- /dev/null +++ b/pkg/extractor/zip/extractor.go @@ -0,0 +1,67 @@ +package zip + +import ( + "archive/zip" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +type Extractor struct{} + +func New() Extractor { return Extractor{} } + +func (e Extractor) Extract(src string, dest string) (files []string, err error) { + r, err := zip.OpenReader(src) + if err != nil { + return files, err + } + defer r.Close() + + for _, f := range r.File { + path := filepath.Join(dest, f.Name) + + // negate ZipSlip vulnerability (http://bit.ly/2MsjAWE) + if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) { + log.Printf("warning: %s is illegal path", path) + continue + } + // remake directory + if f.FileInfo().IsDir() { + if err := os.MkdirAll(path, os.ModePerm); err != nil { + log.Printf("error: %v", err) + } + continue + } + // make file + if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { + log.Printf("error: %v", err) + continue + } + out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + log.Printf("error: %v", err) + continue + } + rc, err := f.Open() + if err != nil { + log.Printf("error: %v", err) + continue + } + + if _, err = io.Copy(out, rc); err != nil { + log.Printf("error: %v", err) + _ = out.Close() + _ = rc.Close() + continue + } + + _ = out.Close() + _ = rc.Close() + + files = append(files, path) + } + return files, nil +} diff --git a/pkg/util/codec.go b/pkg/util/codec.go index 56ff6aa3..4879c132 100644 --- a/pkg/util/codec.go +++ b/pkg/util/codec.go @@ -7,7 +7,7 @@ import ( "os/user" "unsafe" - "github.com/giongto35/cloud-game/v2/pkg/config" + "github.com/giongto35/cloud-game/v2/pkg/encoder" ) // https://stackoverflow.com/questions/9465815/rgb-to-yuv420-algorithm-efficiency @@ -87,6 +87,6 @@ func savePath(hash string) string { // GetVideoEncoder returns video encoder based on some qualification. // Actually Android is only supporting VP8 but H264 has better encoding performance // TODO: Better use useragent attribute from frontend -func GetVideoEncoder(isMobile bool) string { - return config.CODEC_VP8 +func GetVideoEncoder(isMobile bool) encoder.VideoCodec { + return encoder.VPX } diff --git a/pkg/webrtc/ice.go b/pkg/webrtc/ice.go new file mode 100644 index 00000000..e94c8abb --- /dev/null +++ b/pkg/webrtc/ice.go @@ -0,0 +1,57 @@ +package webrtc + +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() +} diff --git a/pkg/webrtc/ice_test.go b/pkg/webrtc/ice_test.go new file mode 100644 index 00000000..2a64f7da --- /dev/null +++ b/pkg/webrtc/ice_test.go @@ -0,0 +1,78 @@ +package webrtc + +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...) + } + }) + } +} diff --git a/pkg/webrtc/webrtc.go b/pkg/webrtc/webrtc.go index 52498cb8..1f1aacc7 100644 --- a/pkg/webrtc/webrtc.go +++ b/pkg/webrtc/webrtc.go @@ -10,7 +10,8 @@ import ( "runtime/debug" "time" - "github.com/giongto35/cloud-game/v2/pkg/config" + webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc" + "github.com/giongto35/cloud-game/v2/pkg/encoder" "github.com/giongto35/cloud-game/v2/pkg/util" "github.com/gofrs/uuid" "github.com/pion/webrtc/v2" @@ -35,6 +36,7 @@ type WebRTC struct { ID string connection *webrtc.PeerConnection + cfg webrtcConfig.Config isConnected bool isClosed bool // for yuvI420 image @@ -100,6 +102,11 @@ func NewWebRTC() *WebRTC { return w } +func (w *WebRTC) WithConfig(conf webrtcConfig.Config) *WebRTC { + w.cfg = conf + return w +} + // StartClient start webrtc func (w *WebRTC) StartClient(isMobile bool, iceCB OnIceCallback) (string, error) { defer func() { @@ -124,7 +131,7 @@ func (w *WebRTC) StartClient(isMobile bool, iceCB OnIceCallback) (string, error) } // add video track - if util.GetVideoEncoder(isMobile) == config.CODEC_H264 { + if util.GetVideoEncoder(isMobile) == encoder.H264 { videoTrack, err = w.connection.NewTrack(webrtc.DefaultPayloadTypeH264, rand.Uint32(), "video", "game-video") } else { videoTrack, err = w.connection.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "game-video") @@ -343,14 +350,13 @@ func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, opusTrack *webrtc.Track) } }() + opusSamples := uint32(w.cfg.Encoder.Audio.GetFrameDuration() / w.cfg.Encoder.Audio.Channels) + for data := range w.AudioChannel { if !w.isConnected { return } - err := opusTrack.WriteSample(media.Sample{ - Data: data, - Samples: uint32(config.AUDIO_FRAME / config.AUDIO_CHANNELS), - }) + err := opusTrack.WriteSample(media.Sample{Data: data, Samples: opusSamples}) if err != nil { log.Println("Warn: Err write sample: ", err) } diff --git a/pkg/worker/coordinator.go b/pkg/worker/coordinator.go index 07d91601..48f12264 100644 --- a/pkg/worker/coordinator.go +++ b/pkg/worker/coordinator.go @@ -6,7 +6,9 @@ import ( "strconv" "github.com/giongto35/cloud-game/v2/pkg/api" + webrtcConfig "github.com/giongto35/cloud-game/v2/pkg/config/webrtc" "github.com/giongto35/cloud-game/v2/pkg/cws" + "github.com/giongto35/cloud-game/v2/pkg/encoder" "github.com/giongto35/cloud-game/v2/pkg/games" "github.com/giongto35/cloud-game/v2/pkg/util" "github.com/giongto35/cloud-game/v2/pkg/webrtc" @@ -14,13 +16,14 @@ import ( "github.com/gorilla/websocket" ) -// CoordinatorClient maintans connection to coordinator -// We expect only one CoordinatorClient for each server +// CoordinatorClient maintains connection to coordinator. +// We expect only one CoordinatorClient for each server. type CoordinatorClient struct { *cws.Client } -// NewCoordinatorClient returns a client connecting to coordinator for coordiation between different server +// NewCoordinatorClient returns a client connecting to coordinator +// for coordination between different server. func NewCoordinatorClient(oc *websocket.Conn) *CoordinatorClient { if oc == nil { return nil @@ -32,276 +35,248 @@ func NewCoordinatorClient(oc *websocket.Conn) *CoordinatorClient { return oClient } -// RouteCoordinator are all routes server received from coordinator +// RouteCoordinator are all routes server received from coordinator. func (h *Handler) RouteCoordinator() { - // iceCandidates := map[string][]string{} oClient := h.oClient /* Coordinator */ // Received from coordinator the serverID - oClient.Receive( - "serverID", - func(response cws.WSPacket) (request cws.WSPacket) { - // Stick session with serverID got from coordinator - log.Println("Received serverID ", response.Data) - h.serverID = response.Data + oClient.Receive("serverID", func(response cws.WSPacket) (request cws.WSPacket) { + // Stick session with serverID got from coordinator + log.Println("Received serverID ", response.Data) + h.serverID = response.Data - return cws.EmptyPacket - }, - ) + return cws.EmptyPacket + }) /* WebRTC Connection */ - oClient.Receive( - "initwebrtc", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received a request to createOffer from browser via coordinator") - - peerconnection := webrtc.NewWebRTC() - var initPacket struct { - IsMobile bool `json:"is_mobile"` - } - err := json.Unmarshal([]byte(resp.Data), &initPacket) - if err != nil { - log.Println("Error: Cannot decode json:", err) - return cws.EmptyPacket - } - - localSession, err := peerconnection.StartClient( - initPacket.IsMobile, - func(candidate string) { - // send back candidate string to browser - oClient.Send(cws.WSPacket{ - ID: "candidate", - Data: candidate, - SessionID: resp.SessionID, - }, nil) - }, - ) - - // localSession, err := peerconnection.StartClient(initPacket.IsMobile, iceCandidates[resp.SessionID]) - // h.peerconnections[resp.SessionID] = peerconnection - - // Create new sessions when we have new peerconnection initialized - session := &Session{ - peerconnection: peerconnection, - } - h.sessions[resp.SessionID] = session - log.Println("Start peerconnection", resp.SessionID) - - if err != nil { - log.Println("Error: Cannot create new webrtc session", err) - return cws.EmptyPacket - } - - return cws.WSPacket{ - ID: "offer", - Data: localSession, - } - }, - ) - - oClient.Receive( - "answer", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received answer SDP from browser") - session := h.getSession(resp.SessionID) - - if session != nil { - peerconnection := session.peerconnection - - err := peerconnection.SetRemoteSDP(resp.Data) - if err != nil { - log.Println("Error: Cannot set RemoteSDP of client: " + resp.SessionID) - } - } else { - log.Printf("Error: No session for ID: %s\n", resp.SessionID) - } + oClient.Receive("initwebrtc", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a request to createOffer from browser via coordinator") + peerconnection := webrtc.NewWebRTC().WithConfig( + webrtcConfig.Config{Encoder: h.cfg.Encoder, Webrtc: h.cfg.Webrtc}, + ) + var initPacket struct { + IsMobile bool `json:"is_mobile"` + } + err := json.Unmarshal([]byte(resp.Data), &initPacket) + if err != nil { + log.Println("Error: Cannot decode json:", err) return cws.EmptyPacket - }, - ) + } - oClient.Receive( - "candidate", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received remote Ice Candidate from browser") - session := h.getSession(resp.SessionID) + localSession, err := peerconnection.StartClient( + initPacket.IsMobile, + func(candidate string) { + // send back candidate string to browser + oClient.Send(cws.WSPacket{ + ID: "candidate", + Data: candidate, + SessionID: resp.SessionID, + }, nil) + }, + ) - if session != nil { - peerconnection := session.peerconnection + // localSession, err := peerconnection.StartClient(initPacket.IsMobile, iceCandidates[resp.SessionID]) + // h.peerconnections[resp.SessionID] = peerconnection - err := peerconnection.AddCandidate(resp.Data) - if err != nil { - log.Println("Error: Cannot add IceCandidate of client: " + resp.SessionID) - } - } else { - log.Printf("Error: No session for ID: %s\n", resp.SessionID) - } + // Create new sessions when we have new peerconnection initialized + session := &Session{ + peerconnection: peerconnection, + } + h.sessions[resp.SessionID] = session + log.Println("Start peerconnection", resp.SessionID) + if err != nil { + log.Println("Error: Cannot create new webrtc session", err) return cws.EmptyPacket - }, - ) + } + + return cws.WSPacket{ + ID: "offer", + Data: localSession, + } + }) + + oClient.Receive("answer", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received answer SDP from browser") + session := h.getSession(resp.SessionID) + + if session != nil { + peerconnection := session.peerconnection + + err := peerconnection.SetRemoteSDP(resp.Data) + if err != nil { + log.Println("Error: Cannot set RemoteSDP of client: " + resp.SessionID) + } + } else { + log.Printf("Error: No session for ID: %s\n", resp.SessionID) + } + + return cws.EmptyPacket + }) + + oClient.Receive("candidate", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received remote Ice Candidate from browser") + session := h.getSession(resp.SessionID) + + if session != nil { + peerconnection := session.peerconnection + + err := peerconnection.AddCandidate(resp.Data) + if err != nil { + log.Println("Error: Cannot add IceCandidate of client: " + resp.SessionID) + } + } else { + log.Printf("Error: No session for ID: %s\n", resp.SessionID) + } + + return cws.EmptyPacket + }) /* Game Logic */ - oClient.Receive( - "start", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received a start request from coordinator") - session := h.getSession(resp.SessionID) - if session == nil { - log.Printf("Error: No session for ID: %s\n", resp.SessionID) - return cws.EmptyPacket - } - - peerconnection := session.peerconnection - // TODO: Standardize for all types of packet. Make WSPacket generic - startPacket := api.GameStartCall{} - if err := startPacket.From(resp.Data); err != nil { - return cws.EmptyPacket - } - gameMeta := games.GameMetadata{ - Name: startPacket.Name, - Type: startPacket.Type, - Path: startPacket.Path, - } - - room := h.startGameHandler(gameMeta, resp.RoomID, resp.PlayerIndex, peerconnection, util.GetVideoEncoder(false)) - session.RoomID = room.ID - // TODO: can data race - h.rooms[room.ID] = room - - return cws.WSPacket{ - ID: "start", - RoomID: room.ID, - } - }, - ) - - oClient.Receive( - "quit", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received a quit request from coordinator") - session := h.getSession(resp.SessionID) - - if session != nil { - room := h.getRoom(session.RoomID) - // Defensive coding, check if the peerconnection is in room - if room.IsPCInRoom(session.peerconnection) { - h.detachPeerConn(session.peerconnection) - } - } else { - log.Printf("Error: No session for ID: %s\n", resp.SessionID) - } - + oClient.Receive("start", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a start request from coordinator") + session := h.getSession(resp.SessionID) + if session == nil { + log.Printf("Error: No session for ID: %s\n", resp.SessionID) return cws.EmptyPacket - }, - ) + } - oClient.Receive( - "save", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received a save game from coordinator") - log.Println("RoomID:", resp.RoomID) - req.ID = "save" - req.Data = "ok" - if resp.RoomID != "" { - room := h.getRoom(resp.RoomID) - if room == nil { - return - } - err := room.SaveGame() - if err != nil { - log.Println("[!] Cannot save game state: ", err) - req.Data = "error" - } - } else { - req.Data = "error" - } + peerconnection := session.peerconnection + // TODO: Standardize for all types of packet. Make WSPacket generic + startPacket := api.GameStartCall{} + if err := startPacket.From(resp.Data); err != nil { + return cws.EmptyPacket + } + gameMeta := games.GameMetadata{ + Name: startPacket.Name, + Type: startPacket.Type, + Path: startPacket.Path, + } - return req - }) + room := h.startGameHandler(gameMeta, resp.RoomID, resp.PlayerIndex, peerconnection, util.GetVideoEncoder(false)) + session.RoomID = room.ID + // TODO: can data race + h.rooms[room.ID] = room - oClient.Receive( - "load", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received a load game from coordinator") - log.Println("Loading game state") - req.ID = "load" - req.Data = "ok" - if resp.RoomID != "" { - room := h.getRoom(resp.RoomID) - err := room.LoadGame() - if err != nil { - log.Println("[!] Cannot load game state: ", err) - req.Data = "error" - } - } else { - req.Data = "error" - } + return cws.WSPacket{ + ID: "start", + RoomID: room.ID, + } + }) - return req - }) + oClient.Receive("quit", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a quit request from coordinator") + session := h.getSession(resp.SessionID) - oClient.Receive( - "playerIdx", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received an update player index event from coordinator") - req.ID = "playerIdx" - - room := h.getRoom(resp.RoomID) - session := h.getSession(resp.SessionID) - idx, err := strconv.Atoi(resp.Data) - log.Printf("Got session %v and room %v", session, room) - - if room != nil && session != nil && err == nil { - room.UpdatePlayerIndex(session.peerconnection, idx) - req.Data = strconv.Itoa(idx) - } else { - req.Data = "error" - } - - return req - }) - - oClient.Receive( - "multitap", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received a multitap toggle from coordinator") - req.ID = "multitap" - req.Data = "ok" - if resp.RoomID != "" { - room := h.getRoom(resp.RoomID) - err := room.ToggleMultitap() - if err != nil { - log.Println("[!] Could not toggle multitap state: ", err) - req.Data = "error" - } - } else { - req.Data = "error" - } - - return req - }) - - oClient.Receive( - "terminateSession", - func(resp cws.WSPacket) (req cws.WSPacket) { - log.Println("Received a terminate session ", resp.SessionID) - session := h.getSession(resp.SessionID) - if session != nil { - session.Close() - delete(h.sessions, resp.SessionID) + if session != nil { + room := h.getRoom(session.RoomID) + // Defensive coding, check if the peerconnection is in room + if room.IsPCInRoom(session.peerconnection) { h.detachPeerConn(session.peerconnection) - } else { - log.Printf("Error: No session for ID: %s\n", resp.SessionID) } + } else { + log.Printf("Error: No session for ID: %s\n", resp.SessionID) + } - return cws.EmptyPacket - }, - ) + return cws.EmptyPacket + }) + + oClient.Receive("save", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a save game from coordinator") + log.Println("RoomID:", resp.RoomID) + req.ID = "save" + req.Data = "ok" + if resp.RoomID != "" { + room := h.getRoom(resp.RoomID) + if room == nil { + return + } + err := room.SaveGame() + if err != nil { + log.Println("[!] Cannot save game state: ", err) + req.Data = "error" + } + } else { + req.Data = "error" + } + + return req + }) + + oClient.Receive("load", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a load game from coordinator") + log.Println("Loading game state") + req.ID = "load" + req.Data = "ok" + if resp.RoomID != "" { + room := h.getRoom(resp.RoomID) + err := room.LoadGame() + if err != nil { + log.Println("[!] Cannot load game state: ", err) + req.Data = "error" + } + } else { + req.Data = "error" + } + + return req + }) + + oClient.Receive("playerIdx", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received an update player index event from coordinator") + req.ID = "playerIdx" + + room := h.getRoom(resp.RoomID) + session := h.getSession(resp.SessionID) + idx, err := strconv.Atoi(resp.Data) + log.Printf("Got session %v and room %v", session, room) + + if room != nil && session != nil && err == nil { + room.UpdatePlayerIndex(session.peerconnection, idx) + req.Data = strconv.Itoa(idx) + } else { + req.Data = "error" + } + + return req + }) + + oClient.Receive("multitap", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a multitap toggle from coordinator") + req.ID = "multitap" + req.Data = "ok" + if resp.RoomID != "" { + room := h.getRoom(resp.RoomID) + err := room.ToggleMultitap() + if err != nil { + log.Println("[!] Could not toggle multitap state: ", err) + req.Data = "error" + } + } else { + req.Data = "error" + } + + return req + }) + + oClient.Receive("terminateSession", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a terminate session ", resp.SessionID) + session := h.getSession(resp.SessionID) + if session != nil { + session.Close() + delete(h.sessions, resp.SessionID) + h.detachPeerConn(session.peerconnection) + } else { + log.Printf("Error: No session for ID: %s\n", resp.SessionID) + } + + return cws.EmptyPacket + }) } func getServerIDOfRoom(oc *CoordinatorClient, roomID string) string { @@ -318,7 +293,7 @@ func getServerIDOfRoom(oc *CoordinatorClient, roomID string) string { } // startGameHandler starts a game if roomID is given, if not create new room -func (h *Handler) startGameHandler(game games.GameMetadata, existedRoomID string, playerIndex int, peerconnection *webrtc.WebRTC, videoEncoderType string) *room.Room { +func (h *Handler) startGameHandler(game games.GameMetadata, existedRoomID string, playerIndex int, peerconnection *webrtc.WebRTC, videoCodec encoder.VideoCodec) *room.Room { log.Printf("Loading game: %v\n", game.Name) // If we are connecting to coordinator, request corresponding serverID based on roomID // TODO: check if existedRoomID is in the current server @@ -327,7 +302,7 @@ func (h *Handler) startGameHandler(game games.GameMetadata, existedRoomID string if room == nil { log.Println("Got Room from local ", room, " ID: ", existedRoomID) // Create new room and update player index - room = h.createNewRoom(game, existedRoomID, videoEncoderType) + room = h.createNewRoom(game, existedRoomID, videoCodec) room.UpdatePlayerIndex(peerconnection, playerIndex) // Wait for done signal from room diff --git a/pkg/worker/handlers.go b/pkg/worker/handlers.go index 890019fb..77199abf 100644 --- a/pkg/worker/handlers.go +++ b/pkg/worker/handlers.go @@ -8,8 +8,10 @@ import ( "path" "time" - "github.com/giongto35/cloud-game/v2/pkg/config" "github.com/giongto35/cloud-game/v2/pkg/config/worker" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/manager/remotehttp" + "github.com/giongto35/cloud-game/v2/pkg/encoder" + "github.com/giongto35/cloud-game/v2/pkg/environment" "github.com/giongto35/cloud-game/v2/pkg/games" "github.com/giongto35/cloud-game/v2/pkg/util" "github.com/giongto35/cloud-game/v2/pkg/webrtc" @@ -18,11 +20,6 @@ import ( "github.com/gorilla/websocket" ) -const ( - gameboyIndex = "./static/game.html" - debugIndex = "./static/game.html" -) - // Flag to determine if the server is coordinator or not var upgrader = websocket.Upgrader{} @@ -52,7 +49,7 @@ func NewHandler(cfg worker.Config) *Handler { return &Handler{ rooms: map[string]*room.Room{}, sessions: map[string]*Session{}, - coordinatorHost: cfg.CoordinatorAddress, + coordinatorHost: cfg.Worker.Network.CoordinatorAddress, cfg: cfg, onlineStorage: onlineStorage, } @@ -61,7 +58,8 @@ func NewHandler(cfg worker.Config) *Handler { // Run starts a Handler running logic func (h *Handler) Run() { for { - oClient, err := setupCoordinatorConnection(h.coordinatorHost, h.cfg.Zone) + conf := h.cfg.Worker.Network + oClient, err := setupCoordinatorConnection(conf.CoordinatorAddress, conf.Zone, h.cfg) if err != nil { log.Printf("Cannot connect to coordinator. %v Retrying...", err) time.Sleep(time.Second) @@ -77,10 +75,22 @@ func (h *Handler) Run() { } } -func setupCoordinatorConnection(ohost string, zone string) (*CoordinatorClient, error) { - var scheme string +func (h *Handler) Prepare() { + if !h.cfg.Emulator.Libretro.Cores.Repo.Sync { + return + } - if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv { + log.Printf("Starting Libretro cores sync...") + coreManager := remotehttp.NewRemoteHttpManager(h.cfg.Emulator.Libretro) + if err := coreManager.Sync(); err != nil { + log.Printf("error: cores sync has failed, %v", err) + } +} + +func setupCoordinatorConnection(ohost string, zone string, cfg worker.Config) (*CoordinatorClient, error) { + var scheme string + env := cfg.Environment.Get() + if env.AnyOf(environment.Production, environment.Staging) { scheme = "wss" } else { scheme = "ws" @@ -170,12 +180,12 @@ func (h *Handler) detachRoom(roomID string) { // createNewRoom creates a new room // Return nil in case of room is existed -func (h *Handler) createNewRoom(game games.GameMetadata, roomID string, videoEncoderType string) *room.Room { +func (h *Handler) createNewRoom(game games.GameMetadata, roomID string, videoCodec encoder.VideoCodec) *room.Room { // If the roomID is empty, // or the roomID doesn't have any running sessions (room was closed) // we spawn a new room if roomID == "" || !h.isRoomRunning(roomID) { - room := room.NewRoom(roomID, game, videoEncoderType, h.onlineStorage, h.cfg) + room := room.NewRoom(roomID, game, videoCodec, h.onlineStorage, h.cfg) // TODO: Might have race condition h.rooms[room.ID] = room return room diff --git a/pkg/worker/room/media.go b/pkg/worker/room/media.go index 550a3ebd..0483a05a 100644 --- a/pkg/worker/room/media.go +++ b/pkg/worker/room/media.go @@ -4,7 +4,7 @@ import ( "fmt" "log" - "github.com/giongto35/cloud-game/v2/pkg/config" + encoderConfig "github.com/giongto35/cloud-game/v2/pkg/config/encoder" "github.com/giongto35/cloud-game/v2/pkg/encoder" "github.com/giongto35/cloud-game/v2/pkg/encoder/h264encoder" vpxencoder "github.com/giongto35/cloud-game/v2/pkg/encoder/vpx-encoder" @@ -39,13 +39,6 @@ func resample(pcm []int16, targetSize int, srcSampleRate int, dstSampleRate int) return newPCM } -func min(x int, y int) int { - if x < y { - return x - } - return y -} - func (r *Room) startVoice() { // broadcast voice go func() { @@ -70,11 +63,11 @@ func (r *Room) startVoice() { }() } -func (r *Room) startAudio(sampleRate int) { +func (r *Room) startAudio(sampleRate int, audio encoderConfig.Audio) { log.Println("Enter fan audio") - srcSampleRate := sampleRate - enc, err := opus.NewEncoder(config.AUDIO_RATE, 2, opus.AppAudio) + srcSampleRate := sampleRate + enc, err := opus.NewEncoder(audio.Frequency, audio.Channels, opus.AppAudio) if err != nil { log.Println("[!] Cannot create audio encoder", err) } @@ -83,8 +76,8 @@ func (r *Room) startAudio(sampleRate int) { enc.SetBitrateToAuto() enc.SetComplexity(10) - dstBufferSize := config.AUDIO_FRAME - srcBufferSize := dstBufferSize * srcSampleRate / config.AUDIO_RATE + dstBufferSize := audio.GetFrameDuration() + srcBufferSize := dstBufferSize * srcSampleRate / audio.Frequency pcm := make([]int16, srcBufferSize) // 640 * 1000 / 16000 == 40 ms idx := 0 @@ -98,7 +91,7 @@ func (r *Room) startAudio(sampleRate int) { if idx == len(pcm) { data := make([]byte, 1024*2) - dstpcm := resample(pcm, dstBufferSize, srcSampleRate, config.AUDIO_RATE) + dstpcm := resample(pcm, dstBufferSize, srcSampleRate, audio.Frequency) n, err := enc.Encode(dstpcm, data) if err != nil { @@ -126,13 +119,13 @@ func (r *Room) startAudio(sampleRate int) { log.Println("Room ", r.ID, " audio channel closed") } -// startVideo listen from imageChannel and push to Encoder. The output of encoder will be pushed to webRTC -func (r *Room) startVideo(width, height int, videoEncoderType string) { +// startVideo processes imageChannel images with an encoder (codec) then pushes the result to WebRTC. +func (r *Room) startVideo(width, height int, videoCodec encoder.VideoCodec) { var enc encoder.Encoder var err error - log.Println("Video Encoder: ", videoEncoderType) - if videoEncoderType == config.CODEC_H264 { + log.Println("Video Encoder: ", videoCodec) + if videoCodec == encoder.H264 { enc, err = h264encoder.NewH264Encoder(width, height, 1) } else { enc, err = vpxencoder.NewVpxEncoder(width, height, 20, 1200, 5) diff --git a/pkg/worker/room/room.go b/pkg/worker/room/room.go index 75e363d5..27a0d08d 100644 --- a/pkg/worker/room/room.go +++ b/pkg/worker/room/room.go @@ -16,7 +16,6 @@ import ( "strings" "sync" - "github.com/giongto35/cloud-game/v2/pkg/config" "github.com/giongto35/cloud-game/v2/pkg/config/worker" "github.com/giongto35/cloud-game/v2/pkg/emulator" "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/nanoarch" @@ -128,7 +127,7 @@ func NewVideoImporter(roomID string) chan nanoarch.GameFrame { } // NewRoom creates a new room -func NewRoom(roomID string, game games.GameMetadata, videoEncoderType string, onlineStorage *storage.Client, cfg worker.Config) *Room { +func NewRoom(roomID string, game games.GameMetadata, videoCodec encoder.VideoCodec, onlineStorage *storage.Client, cfg worker.Config) *Room { if roomID == "" { roomID = generateRoomID(game.Name) } @@ -161,21 +160,22 @@ func NewRoom(roomID string, game games.GameMetadata, videoEncoderType string, on } // If not then load room or create room from local. - log.Printf("Room %s started. GameName: %s, WithGame: %t", roomID, game.Name, cfg.WithoutGame) + log.Printf("Room %s started. GameName: %s, WithGame: %t", roomID, game.Name, cfg.Encoder.WithoutGame) // Spawn new emulator based on gameName and plug-in all channels - emuName, _ := config.FileTypeToEmulator[game.Type] + emuName := cfg.Emulator.GetEmulatorByRom(game.Type) + libretroConfig := cfg.Emulator.GetLibretroCoreConfig(emuName) - if cfg.WithoutGame { + if cfg.Encoder.WithoutGame { // Run without game, image stream is communicated over unixsocket imageChannel := NewVideoImporter(roomID) - director, _, audioChannel := nanoarch.Init(emuName, roomID, false, inputChannel) + director, _, audioChannel := nanoarch.Init(roomID, false, inputChannel, libretroConfig) room.imageChannel = imageChannel room.director = director room.audioChannel = audioChannel } else { // Run without game, image stream is communicated over image channel - director, imageChannel, audioChannel := nanoarch.Init(emuName, roomID, true, inputChannel) + director, imageChannel, audioChannel := nanoarch.Init(roomID, true, inputChannel, libretroConfig) room.imageChannel = imageChannel room.director = director room.audioChannel = audioChannel @@ -183,20 +183,22 @@ func NewRoom(roomID string, game games.GameMetadata, videoEncoderType string, on gameMeta := room.director.LoadMeta(game.Path) - // nwidth, nheight are the webRTC output size. - // There are currently two approach + // nwidth, nheight are the WebRTC output size var nwidth, nheight int - if cfg.EnableAspectRatio { - baseAspectRatio := float64(gameMeta.BaseWidth) / float64(gameMeta.Height) - nwidth, nheight = resizeToAspect(baseAspectRatio, cfg.Width, cfg.Height) - log.Printf("Viewport size will be changed from %dx%d (%f) -> %dx%d", cfg.Width, cfg.Height, + emu, ar := cfg.Emulator, cfg.Emulator.AspectRatio + + if ar.Keep { + baseAspectRatio := float64(gameMeta.BaseWidth) / float64(ar.Height) + nwidth, nheight = resizeToAspect(baseAspectRatio, ar.Width, ar.Height) + log.Printf("Viewport size will be changed from %dx%d (%f) -> %dx%d", ar.Width, ar.Height, baseAspectRatio, nwidth, nheight) } else { nwidth, nheight = gameMeta.BaseWidth, gameMeta.BaseHeight log.Printf("Viewport custom size is disabled, base size will be used instead %dx%d", nwidth, nheight) } - if cfg.Scale > 1 { - nwidth, nheight = nwidth*cfg.Scale, nheight*cfg.Scale + + if emu.Scale > 1 { + nwidth, nheight = nwidth*emu.Scale, nheight*emu.Scale log.Printf("Viewport size has scaled to %dx%d", nwidth, nheight) } @@ -212,8 +214,8 @@ func NewRoom(roomID string, game games.GameMetadata, videoEncoderType string, on room.director.SetViewport(encoderW, encoderH) // Spawn video and audio encoding for webRTC - go room.startVideo(encoderW, encoderH, videoEncoderType) - go room.startAudio(gameMeta.AudioSampleRate) + go room.startVideo(encoderW, encoderH, videoCodec) + go room.startAudio(gameMeta.AudioSampleRate, cfg.Encoder.Audio) go room.startVoice() room.director.Start() diff --git a/pkg/worker/room/room_test.go b/pkg/worker/room/room_test.go index b26ca68b..24dacbb4 100644 --- a/pkg/worker/room/room_test.go +++ b/pkg/worker/room/room_test.go @@ -19,6 +19,7 @@ import ( "github.com/giongto35/cloud-game/v2/pkg/config" "github.com/giongto35/cloud-game/v2/pkg/config/worker" + "github.com/giongto35/cloud-game/v2/pkg/emulator/libretro/manager/remotehttp" "github.com/giongto35/cloud-game/v2/pkg/encoder" "github.com/giongto35/cloud-game/v2/pkg/games" "github.com/giongto35/cloud-game/v2/pkg/thread" @@ -42,16 +43,13 @@ type roomMockConfig struct { roomName string gamesPath string game games.GameMetadata - codec string + codec encoder.VideoCodec autoGlContext bool } -// Restricts a re-config call -// to only one invocation. -var configOnce sync.Once - // Store absolute path to test games -var whereIsGames = getAppPath() + "assets/games/" +var whereIsGames = getRootPath() + "assets/games/" +var whereIsConfigs = getRootPath() + "configs/" var testTempDir = filepath.Join(os.TempDir(), "cloud-game-core-tests") func init() { @@ -70,7 +68,7 @@ func TestRoom(t *testing.T) { tests := []struct { roomName string game games.GameMetadata - codec string + codec encoder.VideoCodec frames int }{ { @@ -79,7 +77,7 @@ func TestRoom(t *testing.T) { Type: "nes", Path: "Super Mario Bros.nes", }, - codec: config.CODEC_VP8, + codec: encoder.VPX, frames: 5, }, } @@ -95,12 +93,14 @@ func TestRoom(t *testing.T) { waitNFrames(test.frames, room.encoder.GetOutputChan()) room.Close() } + // hack: wait room destruction + time.Sleep(2 * time.Second) } func TestRoomWithGL(t *testing.T) { tests := []struct { game games.GameMetadata - codec string + codec encoder.VideoCodec frames int }{ { @@ -109,7 +109,7 @@ func TestRoomWithGL(t *testing.T) { Type: "n64", Path: "Sample Demo by Florian (PD).z64", }, - codec: config.CODEC_VP8, + codec: encoder.VPX, frames: 50, }, } @@ -125,6 +125,8 @@ func TestRoomWithGL(t *testing.T) { waitNFrames(test.frames, room.encoder.GetOutputChan()) room.Close() } + // hack: wait room destruction + time.Sleep(2 * time.Second) } thread.MainMaybe(run) @@ -155,7 +157,7 @@ func TestAllEmulatorRooms(t *testing.T) { room := getRoomMock(roomMockConfig{ gamesPath: whereIsGames, game: test.game, - codec: config.CODEC_VP8, + codec: encoder.VPX, autoGlContext: autoGlContext, }) t.Logf("The game [%v] has been loaded", test.game.Name) @@ -221,9 +223,18 @@ func dumpCanvas(f *image.RGBA, name string, caption string, path string) { // getRoomMock returns mocked Room struct. func getRoomMock(cfg roomMockConfig) roomMock { - configOnce.Do(func() { fixEmulators(cfg.autoGlContext) }) cfg.game.Path = cfg.gamesPath + cfg.game.Path - room := NewRoom(cfg.roomName, cfg.game, cfg.codec, storage.NewInitClient(), worker.NewDefaultConfig()) + + var conf worker.Config + config.LoadConfig(&conf, whereIsConfigs) + fixEmulators(&conf, cfg.autoGlContext) + // sync cores + coreManager := remotehttp.NewRemoteHttpManager(conf.Emulator.Libretro) + if err := coreManager.Sync(); err != nil { + log.Printf("error: cores sync has failed, %v", err) + } + + room := NewRoom(cfg.roomName, cfg.game, cfg.codec, storage.NewInitClient(), conf) // loop-wait the room initialization var init sync.WaitGroup @@ -246,24 +257,25 @@ func getRoomMock(cfg roomMockConfig) roomMock { } // fixEmulators makes absolute game paths in global GameList and passes GL context config. -func fixEmulators(autoGlContext bool) { - appPath := getAppPath() +// hack: emulator paths should be absolute and visible to the tests. +func fixEmulators(config *worker.Config, autoGlContext bool) { + rootPath := getRootPath() - for k, conf := range config.EmulatorConfig { - conf.Path = appPath + conf.Path - if len(conf.Config) > 0 { - conf.Config = appPath + conf.Config - } + config.Emulator.Libretro.Cores.Paths.Libs = + filepath.FromSlash(rootPath + config.Emulator.Libretro.Cores.Paths.Libs) + config.Emulator.Libretro.Cores.Paths.Configs = + filepath.FromSlash(rootPath + config.Emulator.Libretro.Cores.Paths.Configs) + for k, conf := range config.Emulator.Libretro.Cores.List { if conf.IsGlAllowed && autoGlContext { conf.AutoGlContext = true } - config.EmulatorConfig[k] = conf + config.Emulator.Libretro.Cores.List[k] = conf } } -// getAppPath returns absolute path to the assets directory. -func getAppPath() string { +// getRootPath returns absolute path to the assets directory. +func getRootPath() string { p, _ := filepath.Abs("../../../") return p + string(filepath.Separator) } @@ -288,7 +300,7 @@ func waitNFrames(n int, ch chan encoder.OutFrame) { // benchmarkRoom measures app performance for n emulation frames. // Measure period: the room initialization, n emulated and encoded frames, the room shutdown. -func benchmarkRoom(rom games.GameMetadata, codec string, frames int, suppressOutput bool, b *testing.B) { +func benchmarkRoom(rom games.GameMetadata, codec encoder.VideoCodec, frames int, suppressOutput bool, b *testing.B) { if suppressOutput { log.SetOutput(ioutil.Discard) os.Stdout, _ = os.Open(os.DevNull) @@ -311,7 +323,7 @@ func BenchmarkRoom(b *testing.B) { benches := []struct { system string game games.GameMetadata - codecs []string + codecs []encoder.VideoCodec frames int }{ // warm up @@ -322,7 +334,7 @@ func BenchmarkRoom(b *testing.B) { Type: "gba", Path: "Sushi The Cat.gba", }, - codecs: []string{"vp8"}, + codecs: []encoder.VideoCodec{encoder.VPX}, frames: 50, }, { @@ -332,7 +344,7 @@ func BenchmarkRoom(b *testing.B) { Type: "gba", Path: "Sushi The Cat.gba", }, - codecs: []string{"vp8", "x264"}, + codecs: []encoder.VideoCodec{encoder.VPX, encoder.H264}, frames: 100, }, { @@ -342,14 +354,14 @@ func BenchmarkRoom(b *testing.B) { Type: "nes", Path: "Super Mario Bros.nes", }, - codecs: []string{"vp8", "x264"}, + codecs: []encoder.VideoCodec{encoder.VPX, encoder.H264}, frames: 100, }, } for _, bench := range benches { for _, codec := range bench.codecs { - b.Run(fmt.Sprintf("%s-%s-%d", bench.system, codec, bench.frames), func(b *testing.B) { + b.Run(fmt.Sprintf("%s-%v-%d", bench.system, codec, bench.frames), func(b *testing.B) { benchmarkRoom(bench.game, codec, bench.frames, true, b) }) // hack: wait room destruction diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go index 767b4aa9..8b90e56f 100644 --- a/pkg/worker/worker.go +++ b/pkg/worker/worker.go @@ -5,13 +5,12 @@ import ( "crypto/tls" "fmt" "log" - "net" "net/http" "strconv" "time" - "github.com/giongto35/cloud-game/v2/pkg/config" "github.com/giongto35/cloud-game/v2/pkg/config/worker" + "github.com/giongto35/cloud-game/v2/pkg/environment" "github.com/giongto35/cloud-game/v2/pkg/monitoring" "github.com/golang/glog" "golang.org/x/crypto/acme" @@ -32,7 +31,7 @@ func New(ctx context.Context, cfg worker.Config) *Worker { ctx: ctx, cfg: cfg, - monitoringServer: monitoring.NewServerMonitoring(cfg.MonitoringConfig), + monitoringServer: monitoring.NewServerMonitoring(cfg.Worker.Monitoring), } } @@ -51,6 +50,7 @@ func (o *Worker) RunMonitoringServer() { } func (o *Worker) Shutdown() { + // !to add a proper HTTP(S) server shutdown (cws/handler bad loop) if err := o.monitoringServer.Shutdown(o.ctx); err != nil { glog.Errorln("Failed to shutdown monitoring server") } @@ -92,16 +92,18 @@ func (o *Worker) spawnServer(port int) { var certManager *autocert.Manager var httpsSrv *http.Server - if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv { + mode := o.cfg.Environment.Get() + if mode.AnyOf(environment.Production, environment.Staging) { + serverConfig := o.cfg.Worker.Server httpsSrv = makeHTTPServer() - httpsSrv.Addr = fmt.Sprintf(":%d", *config.HttpsPort) + httpsSrv.Addr = fmt.Sprintf(":%d", serverConfig.HttpsPort) - if *config.HttpsChain == "" || *config.HttpsKey == "" { - *config.HttpsChain = "" - *config.HttpsKey = "" + if serverConfig.HttpsChain == "" || serverConfig.HttpsKey == "" { + serverConfig.HttpsChain = "" + serverConfig.HttpsKey = "" var leurl string - if *config.Mode == config.StagingEnv { + if mode == environment.Staging { leurl = stagingLEURL } else { leurl = acme.LetsEncryptURL @@ -116,17 +118,17 @@ func (o *Worker) spawnServer(port int) { httpsSrv.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate} } - go func() { + go func(chain string, key string) { fmt.Printf("Starting HTTPS server on %s\n", httpsSrv.Addr) - err := httpsSrv.ListenAndServeTLS(*config.HttpsChain, *config.HttpsKey) + err := httpsSrv.ListenAndServeTLS(chain, key) if err != nil { log.Printf("httpsSrv.ListendAndServeTLS() failed with %s", err) } - }() + }(serverConfig.HttpsChain, serverConfig.HttpsKey) } var httpSrv *http.Server - if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv { + if mode.AnyOf(environment.Production, environment.Staging) { httpSrv = makeHTTPToHTTPSRedirectServer() } else { httpSrv = makeHTTPServer() @@ -136,43 +138,41 @@ func (o *Worker) spawnServer(port int) { httpSrv.Handler = certManager.HTTPHandler(httpSrv.Handler) } - httpSrv.Addr = ":" + strconv.Itoa(port) - err := httpSrv.ListenAndServe() - if err != nil { - log.Printf("httpSrv.ListenAndServe() failed with %s", err) + startServer(httpSrv, port) +} + +func startServer(serv *http.Server, startPort int) { + // It's recommend to run one worker on one instance. + // This logic is to make sure more than 1 workers still work + for port, n := startPort, startPort+100; port < n; port++ { + serv.Addr = ":" + strconv.Itoa(port) + err := serv.ListenAndServe() + switch err { + case http.ErrServerClosed: + log.Printf("HTTP(S) server was closed") + return + default: + } + port++ + + if port == n { + log.Printf("error: couldn't find an open port in range %v-%v\n", startPort, port) + } } } // initializeWorker setup a worker func (o *Worker) initializeWorker() { - worker := NewHandler(o.cfg) + wrk := NewHandler(o.cfg) defer func() { log.Println("Close worker") - worker.Close() + wrk.Close() }() - go worker.Run() - port := o.cfg.HttpPort - // It's recommend to run one worker on one instance. - // This logic is to make sure more than 1 workers still work - portsNum := 100 - for { - portsNum-- - l, err := net.Listen("tcp", ":"+strconv.Itoa(port)) - if err != nil { - port++ - continue - } + go wrk.Run() + // will block here + wrk.Prepare() - if portsNum < 1 { - log.Printf("Couldn't find an open port in range %v-%v\n", o.cfg.HttpPort, port) - // Cannot find port - return - } - - _ = l.Close() - - o.spawnServer(port) - } + o.spawnServer(o.cfg.Worker.Server.Port) } diff --git a/web/game.html b/web/game.html deleted file mode 100644 index b4896d1e..00000000 --- a/web/game.html +++ /dev/null @@ -1,150 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-
-
-
-
-
- -
- - - - - -
- -
Arrows(move),ZXCVAS(game ABXYLR),1/2(1st/2nd player),Shift/Enter/K/L(select/start/save/load),F(fullscreen),share(copy sharelink to clipboard)
-
-
-
-
- player choice - -
- -
-
-
- - -
-
-
-
-
-
- - - - -
Oh my god
- -
-
-
-
-
- -
- Settings -
-
- - - - Fork me on GitHub - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/web/index.html b/web/index.html index e69de29b..26001fb6 100644 --- a/web/index.html +++ b/web/index.html @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+
+
+
+ +
+ + + + + +
+ +
Arrows(move),ZXCVAS(game ABXYLR),1/2(1st/2nd player),Shift/Enter/K/L(select/start/save/load),F(fullscreen),share(copy + sharelink to clipboard) +
+
+
+
+
+ player choice + +
+ +
+
+
+ + +
+
+
+
+
+
+ + + + +
Oh my god
+ +
+
+
+
+
+ +
+ Settings +
+
+ + + +Fork me on GitHub + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +