mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 10:35:44 +00:00
Add new Libretro core manager module (#249)
* Add initial external configuration files support. These external configuration files allow changing app params at the runtime without recompilation. * Find config files with specified directory in the tests * Add aspect ratio recalculation config * Clean code * Add new configuration files into the Docker container image * Add shared core and config paths into the Libretro cores config * Split ROM <-> Emulator mapping between workers and coordinators * Extract coordinator config * Add shared worker/coordinator server config * Add explicit embedded shared worker/coordinator struct for auto-config reflection fill * Remove default stun/turn servers from the config * Extract and add new ice servers config structures * Update coordinator config params * Add auto emulation lib loader based on the runtime OS/arch * Update configuration structures * Remove shared config embedding * Add missing network config params * Add game library external config * Remove unused config parameters * Add WebRTC encoder external options * Add user dir for config search * Update config loader * Update config * Add generic downloader with Grab lib implementation * Add a simple file downloader backed by the grab lib * Add initial Libretro core repos abstractions * Expose compression info for Libretro cores repository records * Add pipe-based abstract file downloader * Refactor downloader * Refactor Libretro repos * Add worker coresync stubs * Add multiprocess-safe HTTP-based core manager implementation * Remove Libretro cores from the repo * Keep custom N64 cores in te repo for now * Add Libretro cores repo select in the config * Fix http manager repo switch * Cleanup code * Add greedy Libretro lib loader * Don't crash when arch map is not set * Disable dynamic recompiler for pcsx core by default since it's could cause a crash * Use global Libretro dynalib handler * Shorten the default Libretro cores store path * Update zip extractor implementation * Remove explicit fig lib field markings * Add config note to the README file * Add GitHub repo backend for the core downloader * Fix GitHub repo param list in the manager factory * Add env variables reader with CLOUD_GAME prefix * Re-optimize ice server info struct custom marshaler
This commit is contained in:
parent
7299ecc6d1
commit
1fcf34ee02
95 changed files with 1984 additions and 866 deletions
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
7
Dockerfile
vendored
7
Dockerfile
vendored
|
|
@ -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
|
||||
|
|
|
|||
4
Makefile
vendored
4
Makefile
vendored
|
|
@ -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
|
||||
|
|
|
|||
7
README.md
vendored
7
README.md
vendored
|
|
@ -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)
|
||||
|
|
|
|||
0
assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so → assets/cores/mupen64plus_next_libretro.armv7-neon-hf.so
vendored
Executable file → Normal file
0
assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so → assets/cores/mupen64plus_next_libretro.armv7-neon-hf.so
vendored
Executable file → Normal file
0
assets/emulator/libretro/cores/mupen64plus_next_libretro.dylib → assets/cores/mupen64plus_next_libretro.dylib
vendored
Executable file → Normal file
0
assets/emulator/libretro/cores/mupen64plus_next_libretro.dylib → assets/cores/mupen64plus_next_libretro.dylib
vendored
Executable file → Normal file
1
assets/cores/pcsx_rearmed_libretro.cfg
vendored
Normal file
1
assets/cores/pcsx_rearmed_libretro.cfg
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
pcsx_rearmed_drc = disabled
|
||||
BIN
assets/emulator/libretro/cores/citra_libretro.so
vendored
BIN
assets/emulator/libretro/cores/citra_libretro.so
vendored
Binary file not shown.
BIN
assets/emulator/libretro/cores/desmume_libretro.so
vendored
BIN
assets/emulator/libretro/cores/desmume_libretro.so
vendored
Binary file not shown.
Binary file not shown.
BIN
assets/emulator/libretro/cores/fbneo_libretro.so
vendored
BIN
assets/emulator/libretro/cores/fbneo_libretro.so
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/emulator/libretro/cores/mesen_libretro.so
vendored
BIN
assets/emulator/libretro/cores/mesen_libretro.so
vendored
Binary file not shown.
BIN
assets/emulator/libretro/cores/mess2015_libretro.so
vendored
BIN
assets/emulator/libretro/cores/mess2015_libretro.so
vendored
Binary file not shown.
BIN
assets/emulator/libretro/cores/meteor_libretro.so
vendored
BIN
assets/emulator/libretro/cores/meteor_libretro.so
vendored
Binary file not shown.
Binary file not shown.
BIN
assets/emulator/libretro/cores/mgba_libretro.dll
vendored
BIN
assets/emulator/libretro/cores/mgba_libretro.dll
vendored
Binary file not shown.
BIN
assets/emulator/libretro/cores/mgba_libretro.dylib
vendored
BIN
assets/emulator/libretro/cores/mgba_libretro.dylib
vendored
Binary file not shown.
BIN
assets/emulator/libretro/cores/mgba_libretro.so
vendored
BIN
assets/emulator/libretro/cores/mgba_libretro.so
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/emulator/libretro/cores/nestopia_libretro.dll
vendored
BIN
assets/emulator/libretro/cores/nestopia_libretro.dll
vendored
Binary file not shown.
Binary file not shown.
BIN
assets/emulator/libretro/cores/nestopia_libretro.so
vendored
BIN
assets/emulator/libretro/cores/nestopia_libretro.so
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/emulator/libretro/cores/prosystem_libretro.so
vendored
BIN
assets/emulator/libretro/cores/prosystem_libretro.so
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/emulator/libretro/cores/snes9x_libretro.dll
vendored
BIN
assets/emulator/libretro/cores/snes9x_libretro.dll
vendored
Binary file not shown.
BIN
assets/emulator/libretro/cores/snes9x_libretro.dylib
vendored
BIN
assets/emulator/libretro/cores/snes9x_libretro.dylib
vendored
Binary file not shown.
BIN
assets/emulator/libretro/cores/snes9x_libretro.so
vendored
BIN
assets/emulator/libretro/cores/snes9x_libretro.so
vendored
Binary file not shown.
Binary file not shown.
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
161
configs/config.yaml
vendored
Normal file
161
configs/config.yaml
vendored
Normal file
|
|
@ -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
|
||||
4
go.mod
vendored
4
go.mod
vendored
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
10
go.sum
vendored
10
go.sum
vendored
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
43
pkg/config/coordinator/config.go
Normal file
43
pkg/config/coordinator/config.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
95
pkg/config/emulator/config.go
Normal file
95
pkg/config/emulator/config.go
Normal file
|
|
@ -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
|
||||
}
|
||||
16
pkg/config/encoder/config.go
Normal file
16
pkg/config/encoder/config.go
Normal file
|
|
@ -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
|
||||
}
|
||||
25
pkg/config/loader.go
Normal file
25
pkg/config/loader.go
Normal file
|
|
@ -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
|
||||
}
|
||||
31
pkg/config/shared/config.go
Normal file
31
pkg/config/shared/config.go
Normal file
|
|
@ -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]")
|
||||
}
|
||||
18
pkg/config/webrtc/config.go
Normal file
18
pkg/config/webrtc/config.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
42
pkg/downloader/backend/grab.go
Normal file
42
pkg/downloader/backend/grab.go
Normal file
|
|
@ -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
|
||||
}
|
||||
42
pkg/downloader/downloader.go
Normal file
42
pkg/downloader/downloader.go
Normal file
|
|
@ -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
|
||||
}
|
||||
29
pkg/downloader/pipe/pipe.go
Normal file
29
pkg/downloader/pipe/pipe.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
38
pkg/emulator/libretro/core/core.go
Normal file
38
pkg/emulator/libretro/core/core.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
42
pkg/emulator/libretro/manager/manager.go
Normal file
42
pkg/emulator/libretro/manager/manager.go
Normal file
|
|
@ -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
|
||||
}
|
||||
96
pkg/emulator/libretro/manager/remotehttp/manager.go
Normal file
96
pkg/emulator/libretro/manager/remotehttp/manager.go
Normal file
|
|
@ -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
|
||||
}
|
||||
37
pkg/emulator/libretro/manager/remotehttp/manager_test.go
Normal file
37
pkg/emulator/libretro/manager/remotehttp/manager_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
73
pkg/emulator/libretro/nanoarch/loader.go
Normal file
73
pkg/emulator/libretro/nanoarch/loader.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
package nanoarch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: -ldl
|
||||
#include <stdlib.h>
|
||||
#include <dlfcn.h>
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <dlfcn.h>
|
||||
#include <string.h>
|
||||
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
30
pkg/emulator/libretro/repo/buildbot/repository.go
Normal file
30
pkg/emulator/libretro/repo/buildbot/repository.go
Normal file
|
|
@ -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}
|
||||
}
|
||||
56
pkg/emulator/libretro/repo/buildbot/repository_test.go
Normal file
56
pkg/emulator/libretro/repo/buildbot/repository_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
7
pkg/emulator/libretro/repo/compression.go
Normal file
7
pkg/emulator/libretro/repo/compression.go
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package repo
|
||||
|
||||
type CompressionType string
|
||||
|
||||
func (c *CompressionType) GetExt() string {
|
||||
return (string)(*c)
|
||||
}
|
||||
20
pkg/emulator/libretro/repo/github/repository.go
Normal file
20
pkg/emulator/libretro/repo/github/repository.go
Normal file
|
|
@ -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}
|
||||
}
|
||||
56
pkg/emulator/libretro/repo/github/repository_test.go
Normal file
56
pkg/emulator/libretro/repo/github/repository_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
24
pkg/emulator/libretro/repo/raw/repository.go
Normal file
24
pkg/emulator/libretro/repo/raw/repository.go
Normal file
|
|
@ -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}
|
||||
}
|
||||
14
pkg/emulator/libretro/repo/repository.go
Normal file
14
pkg/emulator/libretro/repo/repository.go
Normal file
|
|
@ -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
|
||||
}
|
||||
)
|
||||
8
pkg/encoder/codec.go
Normal file
8
pkg/encoder/codec.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package encoder
|
||||
|
||||
type VideoCodec int
|
||||
|
||||
const (
|
||||
H264 VideoCodec = iota
|
||||
VPX
|
||||
)
|
||||
18
pkg/environment/env.go
Normal file
18
pkg/environment/env.go
Normal file
|
|
@ -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
|
||||
}
|
||||
24
pkg/extractor/extractor.go
Normal file
24
pkg/extractor/extractor.go
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
67
pkg/extractor/zip/extractor.go
Normal file
67
pkg/extractor/zip/extractor.go
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
57
pkg/webrtc/ice.go
Normal file
57
pkg/webrtc/ice.go
Normal file
|
|
@ -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()
|
||||
}
|
||||
78
pkg/webrtc/ice_test.go
Normal file
78
pkg/webrtc/ice_test.go
Normal file
|
|
@ -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...)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
150
web/game.html
vendored
150
web/game.html
vendored
|
|
@ -1,150 +0,0 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="user-scalable=0">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<meta property="og:type" content="cloud-game" />
|
||||
<meta property="og:title" content="Web-based Cloud Gaming for Retro Games" />
|
||||
<meta property="og:description" content="Play and share cloud gaming experience with your friends" />
|
||||
<meta property="og:image" content="http://cloud.webgame2d.com/static/img/ogimage.jpg" />
|
||||
<meta property="og:url" content="" />
|
||||
<meta property="og:site_name" content="Cloud Retro" />
|
||||
<meta property="og:author" content="giongto35 trichimtrich" />
|
||||
|
||||
<link href="/static/css/font-awesome.css?2" rel="stylesheet">
|
||||
<link href="/static/css/main.css?4" rel="stylesheet">
|
||||
<link href="/static/css/ui.css?v=1" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="gamebody">
|
||||
<!--<div id="ui-emulator-bg"></div>-->
|
||||
|
||||
<div id="circle-pad-holder">
|
||||
<div id="btn-up" class="dpad" value="up"></div>
|
||||
<div id="btn-down" class="dpad" value="down"></div>
|
||||
<div id="btn-left" class="dpad" value="left"></div>
|
||||
<div id="btn-right" class="dpad" value="right"></div>
|
||||
<div id="circle-pad"></div>
|
||||
</div>
|
||||
|
||||
<div id="bottom-screen">
|
||||
<div id="stats-overlay" class="no-select" hidden></div>
|
||||
<!--NOTE: New browser doesn't allow unmuted video player. So we muted here.
|
||||
There is still audio because current audio flow is not from media but it is manually encoded (technical webRTC challenge). Later, when we can integrate audio to media, we can face the issue with mute again .
|
||||
https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
||||
-->
|
||||
<video id="game-screen" muted playinfullscreen="false" playsinline></video>
|
||||
|
||||
<div id="menu-screen">
|
||||
<div id="menu-container">
|
||||
</div>
|
||||
<div id="menu-item-choice"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="guide-txt"><b>Arrows</b>(move),<b>ZXCVAS</b>(game ABXYLR),<b>1/2</b>(1st/2nd player),<b>Shift/Enter/K/L</b>(select/start/save/load),<b>F</b>(fullscreen),<b>share</b>(copy sharelink to clipboard)</div>
|
||||
<div id="btn-load" unselectable="on" class="btn big unselectable" value="load"></div>
|
||||
<div id="btn-save" unselectable="on" class="btn big unselectable" value="save"></div>
|
||||
<div id="btn-join" unselectable="on" class="btn big unselectable" value="join"></div>
|
||||
<div id="slider-playeridx" class="slidecontainer">
|
||||
player choice
|
||||
<input type="range" min="1" max="4" value="1" class="slider" id="playeridx" onkeydown="event.preventDefault()">
|
||||
</div>
|
||||
|
||||
<div id="btn-quit" unselectable="on" class="btn big unselectable" value="quit"></div>
|
||||
<div id="btn-select" unselectable="on" class="btn big unselectable" value="select"></div>
|
||||
<div id="btn-start" unselectable="on" class="btn big unselectable" value="start"></div>
|
||||
|
||||
|
||||
<div id="color-button-holder">
|
||||
<div id="btn-a" unselectable="on" class="btn unselectable" value="a"></div>
|
||||
<div id="btn-b" unselectable="on" class="btn unselectable" value="b"></div>
|
||||
<div id="btn-x" unselectable="on" class="btn unselectable" value="x"></div>
|
||||
<div id="btn-y" unselectable="on" class="btn unselectable" value="y"></div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: remove -->
|
||||
<input id="room-txt" type="text" placeholder="room id..." unselectable="on" class=" unselectable" disabled>
|
||||
|
||||
<div id="noti-box" unselectable="on" class="unselectable">Oh my god</div>
|
||||
|
||||
<div id="help-overlay">
|
||||
<div id="help-overlay-background"></div>
|
||||
<div id="help-overlay-detail"></div>
|
||||
</div>
|
||||
<div id="btn-help" unselectable="on" class="btn unselectable" value="help"></div>
|
||||
|
||||
<div id="btn-settings" class="btn btn2 unselectable" value="settings">
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app-settings" class="modal-window">
|
||||
<div>
|
||||
<div class="settings__controls">
|
||||
<span title="Save" id="settings__controls__save" class="semi-button">↑</span>
|
||||
<span title="Load" id="settings__controls__load" class="semi-button">↓</span>
|
||||
<span title="Reset" id="settings__controls__reset" class="semi-button">⟲</span>
|
||||
<span title="Close" id="settings__controls__close" class="semi-button">X</span>
|
||||
</div>
|
||||
<h1>Settings</h1>
|
||||
<div id="settings-data"></div>
|
||||
<div>
|
||||
* -- applied after application restart
|
||||
</div>
|
||||
</div>
|
||||
<label class="dpad-toggle-label">
|
||||
<input type="checkbox" id="dpad-toggle" checked>
|
||||
<span class="dpad-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a id="ribbon" style="position: fixed; right: 0; top: 0;" href="https://github.com/giongto35/cloud-game"><img width="149" height="149" src="https://github.blog/wp-content/uploads/2008/12/forkme_right_gray_6d6d6d.png?resize=149%2C149" class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>
|
||||
|
||||
|
||||
<script>
|
||||
DEBUG = true;
|
||||
STUNTURN = {{.STUNTURN}};
|
||||
</script>
|
||||
|
||||
<script src="/static/js/lib/jquery-3.4.1.min.js"></script>
|
||||
|
||||
<script src="/static/js/gui/gui.js?v=1"></script>
|
||||
<script src="/static/js/log.js?v=5"></script>
|
||||
<script src="/static/js/utils.js?v1"></script>
|
||||
<script src="/static/js/event/event.js?v=5"></script>
|
||||
<script src="/static/js/input/keys.js?v=3"></script>
|
||||
<script src="/static/js/settings/opts.js?v=1"></script>
|
||||
<script src="/static/js/settings/settings.js?v=2"></script>
|
||||
<script src="/static/js/env.js?v=5"></script>
|
||||
<script src="/static/js/input/input.js?v=3"></script>
|
||||
<script src="/static/js/gameList.js?v=3"></script>
|
||||
<script src="/static/js/room.js?v=3"></script>
|
||||
<script src="/static/js/stats/stats.js?v=1"></script>
|
||||
<script src="/static/js/network/ajax.js?v=3"></script>
|
||||
<script src="/static/js/network/socket.js?v=4"></script>
|
||||
<script src="/static/js/network/rtcp.js?v=3"></script>
|
||||
<script src="/static/js/controller.js?v=6"></script>
|
||||
<script src="/static/js/input/keyboard.js?v=5"></script>
|
||||
<script src="/static/js/input/touch.js?v=3"></script>
|
||||
<script src="/static/js/input/joystick.js?v=3"></script>
|
||||
|
||||
<script src="/static/js/init.js?v=5"></script>
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-145078282-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-145078282-1');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
153
web/index.html
vendored
153
web/index.html
vendored
|
|
@ -0,0 +1,153 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="user-scalable=0">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
<meta property="og:type" content="cloud-game"/>
|
||||
<meta property="og:title" content="Web-based Cloud Gaming for Retro Games"/>
|
||||
<meta property="og:description" content="Play and share cloud gaming experience with your friends"/>
|
||||
<meta property="og:image" content="http://cloud.webgame2d.com/static/img/ogimage.jpg"/>
|
||||
<meta property="og:url" content=""/>
|
||||
<meta property="og:site_name" content="Cloud Retro"/>
|
||||
<meta property="og:author" content="giongto35 trichimtrich"/>
|
||||
|
||||
<link href="/static/css/font-awesome.css?2" rel="stylesheet">
|
||||
<link href="/static/css/main.css?4" rel="stylesheet">
|
||||
<link href="/static/css/ui.css?v=1" rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="gamebody">
|
||||
<!--<div id="ui-emulator-bg"></div>-->
|
||||
|
||||
<div id="circle-pad-holder">
|
||||
<div id="btn-up" class="dpad" value="up"></div>
|
||||
<div id="btn-down" class="dpad" value="down"></div>
|
||||
<div id="btn-left" class="dpad" value="left"></div>
|
||||
<div id="btn-right" class="dpad" value="right"></div>
|
||||
<div id="circle-pad"></div>
|
||||
</div>
|
||||
|
||||
<div id="bottom-screen">
|
||||
<div id="stats-overlay" class="no-select" hidden></div>
|
||||
<!--NOTE: New browser doesn't allow unmuted video player. So we muted here.
|
||||
There is still audio because current audio flow is not from media but it is manually encoded (technical webRTC challenge). Later, when we can integrate audio to media, we can face the issue with mute again .
|
||||
https://developers.google.com/web/updates/2017/09/autoplay-policy-changes
|
||||
-->
|
||||
<video id="game-screen" muted playinfullscreen="false" playsinline></video>
|
||||
|
||||
<div id="menu-screen">
|
||||
<div id="menu-container">
|
||||
</div>
|
||||
<div id="menu-item-choice"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="guide-txt"><b>Arrows</b>(move),<b>ZXCVAS</b>(game ABXYLR),<b>1/2</b>(1st/2nd player),<b>Shift/Enter/K/L</b>(select/start/save/load),<b>F</b>(fullscreen),<b>share</b>(copy
|
||||
sharelink to clipboard)
|
||||
</div>
|
||||
<div id="btn-load" unselectable="on" class="btn big unselectable" value="load"></div>
|
||||
<div id="btn-save" unselectable="on" class="btn big unselectable" value="save"></div>
|
||||
<div id="btn-join" unselectable="on" class="btn big unselectable" value="join"></div>
|
||||
<div id="slider-playeridx" class="slidecontainer">
|
||||
player choice
|
||||
<input type="range" min="1" max="4" value="1" class="slider" id="playeridx" onkeydown="event.preventDefault()">
|
||||
</div>
|
||||
|
||||
<div id="btn-quit" unselectable="on" class="btn big unselectable" value="quit"></div>
|
||||
<div id="btn-select" unselectable="on" class="btn big unselectable" value="select"></div>
|
||||
<div id="btn-start" unselectable="on" class="btn big unselectable" value="start"></div>
|
||||
|
||||
|
||||
<div id="color-button-holder">
|
||||
<div id="btn-a" unselectable="on" class="btn unselectable" value="a"></div>
|
||||
<div id="btn-b" unselectable="on" class="btn unselectable" value="b"></div>
|
||||
<div id="btn-x" unselectable="on" class="btn unselectable" value="x"></div>
|
||||
<div id="btn-y" unselectable="on" class="btn unselectable" value="y"></div>
|
||||
</div>
|
||||
|
||||
<!-- TODO: remove -->
|
||||
<input id="room-txt" type="text" placeholder="room id..." unselectable="on" class=" unselectable" disabled>
|
||||
|
||||
<div id="noti-box" unselectable="on" class="unselectable">Oh my god</div>
|
||||
|
||||
<div id="help-overlay">
|
||||
<div id="help-overlay-background"></div>
|
||||
<div id="help-overlay-detail"></div>
|
||||
</div>
|
||||
<div id="btn-help" unselectable="on" class="btn unselectable" value="help"></div>
|
||||
|
||||
<div id="btn-settings" class="btn btn2 unselectable" value="settings">
|
||||
Settings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="app-settings" class="modal-window">
|
||||
<div>
|
||||
<div class="settings__controls">
|
||||
<span title="Save" id="settings__controls__save" class="semi-button">↑</span>
|
||||
<span title="Load" id="settings__controls__load" class="semi-button">↓</span>
|
||||
<span title="Reset" id="settings__controls__reset" class="semi-button">⟲</span>
|
||||
<span title="Close" id="settings__controls__close" class="semi-button">X</span>
|
||||
</div>
|
||||
<h1>Settings</h1>
|
||||
<div id="settings-data"></div>
|
||||
<div>
|
||||
* -- applied after application restart
|
||||
</div>
|
||||
</div>
|
||||
<label class="dpad-toggle-label">
|
||||
<input type="checkbox" id="dpad-toggle" checked>
|
||||
<span class="dpad-toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a id="ribbon" style="position: fixed; right: 0; top: 0;" href="https://github.com/giongto35/cloud-game"><img
|
||||
width="149" height="149"
|
||||
src="https://github.blog/wp-content/uploads/2008/12/forkme_right_gray_6d6d6d.png?resize=149%2C149"
|
||||
class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>
|
||||
|
||||
<script src="/static/js/lib/jquery-3.4.1.min.js"></script>
|
||||
|
||||
<script src="/static/js/gui/gui.js?v=1"></script>
|
||||
<script src="/static/js/log.js?v=5"></script>
|
||||
<script src="/static/js/utils.js?v1"></script>
|
||||
<script src="/static/js/event/event.js?v=5"></script>
|
||||
<script src="/static/js/input/keys.js?v=3"></script>
|
||||
<script src="/static/js/settings/opts.js?v=1"></script>
|
||||
<script src="/static/js/settings/settings.js?v=2"></script>
|
||||
<script src="/static/js/env.js?v=5"></script>
|
||||
<script src="/static/js/input/input.js?v=3"></script>
|
||||
<script src="/static/js/gameList.js?v=3"></script>
|
||||
<script src="/static/js/room.js?v=3"></script>
|
||||
<script src="/static/js/stats/stats.js?v=1"></script>
|
||||
<script src="/static/js/network/ajax.js?v=3"></script>
|
||||
<script src="/static/js/network/socket.js?v=4"></script>
|
||||
<script src="/static/js/network/rtcp.js?v=3"></script>
|
||||
<script src="/static/js/controller.js?v=6"></script>
|
||||
<script src="/static/js/input/keyboard.js?v=5"></script>
|
||||
<script src="/static/js/input/touch.js?v=3"></script>
|
||||
<script src="/static/js/input/joystick.js?v=3"></script>
|
||||
|
||||
<script src="/static/js/init.js?v=5"></script>
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-145078282-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
function gtag() {
|
||||
dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-145078282-1');
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue