Compare commits

...

484 commits

Author SHA1 Message Date
sergystepanov
9e6efc2319 Update retropad input 2025-12-30 14:36:48 +03:00
sergystepanov
1d5bae0c62 Add analog triggers and pack axes into atomic int64
- Pack 4 analog axes (LX, LY, RX, RY) into single int64 for atomic access
- Pack L2/R2 analog triggers into single int32
- Reduce memory per port from 20 to 16 bytes
- Reduce atomic stores per SetInput from 5 to 3
- Add RETRO_DEVICE_INDEX_ANALOG_BUTTON support for analog trigger queries
- Fallback to digital (0/0x7FFF) for non-trigger analog button queries

Wire format: [BTN:2][LX:2][LY:2][RX:2][RY:2][L2:2][R2:2] (14 bytes)
2025-12-29 20:20:55 +03:00
sergystepanov
368bae8c07 Swap mutex to atomics in keyboard input 2025-12-28 21:25:33 +03:00
sergystepanov
58a19affcb Clean SDL/OpenGL functions 2025-12-27 01:58:14 +03:00
sergystepanov
8754a5edfa Tweak OpenGL framebuffer
Force alignment for GL ReadPixels and skip unbinding last framebuffer which makes it a bit faster.
2025-12-26 16:17:37 +03:00
sergystepanov
aeb41008c9 Remove room watchers 2025-12-24 21:25:03 +03:00
sergystepanov
059e19d790 Remove com.Uid from the API 2025-12-24 21:23:19 +03:00
sergystepanov
baa9bad6f8 Update dependencies 2025-12-22 15:38:17 +03:00
sergystepanov
94e13cb93b Clean api 2025-12-22 15:37:04 +03:00
sergystepanov
c800dd4bf9 Fix with go fix 2025-12-22 15:08:50 +03:00
sergystepanov
7c91d200e4 Update Go version to 1.26rc1 2025-12-17 23:12:50 +03:00
sergystepanov
d45daeab7a Tweak room join/creation logic 2025-12-15 18:42:41 +03:00
sergystepanov
b3ccea5f0e Refactor media buffer
Mostly cleanup.
2025-12-15 15:52:46 +03:00
sergystepanov
3178086dd7 Revert due to weird 32KHz mGBA issues
(fix later)
2025-12-14 22:29:27 +03:00
sergystepanov
1e4e5b3c65 Clean media buffer 2025-12-14 22:15:28 +03:00
sergystepanov
7c8e74716d Disable mGBA low-pass filter 2025-12-14 22:14:55 +03:00
sergystepanov
46a5799079 Fix media tests 2025-12-14 18:54:06 +03:00
sergystepanov
9feb788108 Make speexdsp statically linked 2025-12-14 17:01:04 +03:00
sergystepanov
e2f3e005ef Fix speex build libs 2025-12-14 16:31:13 +03:00
sergystepanov
9d54ea4c49 Add and use Speex audio resampler 2025-12-14 16:24:35 +03:00
sergystepanov
671e875f12 Add input cache for retropad, keyboard and mouse 2025-12-14 13:53:21 +03:00
sergystepanov
f708fce112 Revert "Try atomic-based locks in the same thread execution loop instead of a bunch of mutexes."
This reverts commit 460c466053.
2025-12-14 13:30:45 +03:00
sergystepanov
460c466053 Try atomic-based locks in the same thread execution loop instead of a bunch of mutexes. 2025-12-14 13:18:34 +03:00
sergystepanov
84ad0a4cac Add audio resampling option
You can now select between linear interpolation and nearest-neighbor resampling algorithms.
2025-12-13 23:56:38 +03:00
sergystepanov
129690e901 Fix map test 2025-11-22 22:21:05 +03:00
sergystepanov
9191861cab Use iterators in the custom map implementation 2025-11-22 22:09:38 +03:00
sergystepanov
c05e42f597 Cleanup nanoarch.go 2025-11-22 21:20:16 +03:00
sergystepanov
09a0c9c3f2 Revert "Add user input caching"
This reverts commit 859d0c8f1a.
2025-11-22 17:46:07 +03:00
sergystepanov
859d0c8f1a Add user input caching 2025-11-22 17:22:40 +03:00
sergystepanov
baaeaf43b1 Add config option for logging dropped frames 2025-11-22 12:32:56 +03:00
sergystepanov
76b376aef7 Add config option for skipping late video frames 2025-11-22 11:59:08 +03:00
sergystepanov
3df6a24a0a Skip video frames when they are late 2025-11-21 22:35:33 +03:00
sergystepanov
efa7a1d7b5 Update outdated Docker build 2025-11-21 20:44:27 +03:00
sergystepanov
5c6406c1e7 Implemented a busy loop for the emulation ticker.
This replaces the low-precision, OS-dependent time ticker with a CPU spin loop that performs continuous target frame time checks and corrections.
2025-11-21 20:13:27 +03:00
sergystepanov
3392251dda Update dependencies 2025-11-20 00:36:13 +03:00
sergystepanov
bbad4539b1 Update libretro.h 2025-11-20 00:33:03 +03:00
sergystepanov
6b0d7c0ce1 Update Go to 1.25.0 2025-08-15 14:51:20 +03:00
sergystepanov
e03fbadcaa Update dependencies
go: upgraded github.com/VictoriaMetrics/metrics v1.38.0 => v1.39.1
go: upgraded github.com/go-viper/mapstructure/v2 v2.3.0 => v2.4.0
go: upgraded github.com/klauspost/cpuid/v2 v2.2.10 => v2.3.0
go: upgraded github.com/knadh/koanf/v2 v2.2.1 => v2.2.2
go: upgraded github.com/minio/crc64nvme v1.0.2 => v1.1.0
go: upgraded github.com/minio/minio-go/v7 v7.0.94 => v7.0.95
go: upgraded github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c => v1.2.0
go: upgraded github.com/pion/logging v0.2.3 => v0.2.4
go: upgraded github.com/pion/rtp v1.8.19 => v1.8.21
go: upgraded github.com/pion/sdp/v3 v3.0.14 => v3.0.15
go: upgraded github.com/pion/turn/v4 v4.0.2 => v4.1.0
go: upgraded github.com/pion/webrtc/v4 v4.1.2 => v4.1.3
go: upgraded golang.org/x/crypto v0.39.0 => v0.41.0
go: upgraded golang.org/x/net v0.41.0 => v0.43.0
go: upgraded golang.org/x/sys v0.33.0 => v0.35.0
go: upgraded golang.org/x/text v0.26.0 => v0.28.0
2025-08-08 19:41:17 +03:00
sergystepanov
42b003db62 Verifies during startup if the system can run the emulator
This check can be disabled with the emulator.failFast = false config option. Right now it checks SDL2 video context creation.
2025-06-20 18:30:17 +03:00
sergystepanov
d8eed66a1d Update dependencies 2025-06-20 18:30:17 +03:00
sergystepanov
8083ba086b Use /usr/bin/env in the shebang line of shell scripts to ensure portability 2025-05-28 08:19:02 +03:00
sergystepanov
02210f1f8d Remove unnecessary C bridge functions 2025-05-18 12:46:32 +03:00
sergystepanov
817a19c757 Fix the circle-pad's roundness 2025-05-16 12:15:37 +03:00
sergystepanov
36da07f277 Use the actual state size when loading ROMs 2025-05-03 16:35:21 +03:00
sergystepanov
83056bbf4f Update dependencies 2025-05-03 15:57:41 +03:00
sergystepanov
37a4a80996 Use the save state size before each save/load call 2025-05-03 15:55:26 +03:00
sergystepanov
9d4256306e Add missing C function header for Go debugger 2025-05-02 10:34:22 +03:00
sergystepanov
ddfc9249ec Fix some user slot race conditions
In cases where HasSlot() and Reserve() operations are delayed, multiple users may incorrectly be granted a slot due to race conditions.
2025-05-02 10:06:23 +03:00
sergystepanov
a431b7050f Update dependencies 2025-04-17 09:11:26 +03:00
sergystepanov
debd4b23df Disable default static build
It is broken with SDL2 on Windows
2025-04-17 09:11:26 +03:00
sergystepanov
410610349b Switch to UCRT toolchain in MSYS2 2025-04-17 09:11:26 +03:00
sergystepanov
3ac7a559df Skip YUV test
It is broken on Windows
2025-04-17 09:11:26 +03:00
sergystepanov
7c878b1ee3 Update dependencies 2025-04-17 09:11:26 +03:00
sergystepanov
a1506d0f31 Add PGO with 1.24.0 2025-02-13 01:00:13 +03:00
sergystepanov
15ff2f3282 Update Go to 1.24.0 2025-02-12 14:17:43 +03:00
sergystepanov
ffb0abe4da Update dependencies 2025-01-18 19:41:21 +03:00
sergystepanov
3dbf4f9b19 Update dependencies 2025-01-11 16:53:52 +03:00
sergystepanov
b02cd5c4f0
It is time to update the copyright year 2025-01-04 10:41:51 +03:00
Sergey Stepanov
0c768bb3d6
Add some notes on recording in regards to ffconcat 2024-12-21 01:37:42 +03:00
Sergey Stepanov
f78bcf3e4b
Allow config for the remote Libretro core repos 2024-12-20 01:32:20 +03:00
Sergey Stepanov
535e725618
Panic when dlib functions are missing 2024-12-19 21:40:14 +03:00
Sergey Stepanov
4aaeda3fbb
Move some RETRO_ENVIRONMENT vars into C 2024-12-16 23:28:14 +03:00
Sergey Stepanov
600243c87d Update dependencies 2024-12-16 13:48:34 +03:00
Sergey Stepanov
82aebf6647 Fix Package 'libgl1-mesa-glx' has no installation candidate 2024-12-14 14:14:56 +03:00
Sergey Stepanov
89ae98b035 Why do we need samples
In an ideal scenario, the emulator generates a video frame and an audio chunk with its internal frame rate. For example, if the emulator runs a game at 60 FPS, it will produce 16 ms worth of audio and a video frame with each tick (or call of the run function). Then we need to send all this data to the user's browser, which becomes tricky with WebRTC audio.

The WebRTC standard supports only Opus-encoded audio for high-quality sound. The encoder and decoder (the audio player in the browser) have a limitation: they can only operate on fixed audio frames or predefined chunks of audio, which are 5, 10, 20, 40, or 60 ms in length.

Due to this limitation, we have to wait at least two ticks until the first whole audio chunk can be packed into predefined frames. If we have 16 ms of audio and one fixed buffer, we send 10 ms right away and have to wait for 4 ms to add to the remaining 6 ms. This will lead to a constant 6 ms delay between audio and video.

To mitigate this issue, we can set the smallest frame size as a buffer, i.e., 5 ms. This will decrease the latency to 1 ms, but we will send 3 packets of data in this manner for 16 ms.

A slightly better way is to create several buffers and dynamically select the next buffer so that the audio fits optimally, minimizing the number of network packets sent to users.

This frames thing essentially accomplishes that. In the options, we can select multiple (or one) Opus buffers to store audio and choose from. They should be defined from the largest to the smallest. And that's it.
2024-12-13 18:57:25 +03:00
sergystepanov
ed3b195b26
Dynamic audio buf
* Ugly audio buf

* Use dynamic Opus frames with config
2024-12-12 21:13:43 +03:00
Sergey Stepanov
f54089e072
Stretch samples a bit better with the GBA's 32768Hz 2024-12-07 00:47:27 +03:00
Sergey Stepanov
6bb82b2204 Allow 2.5ms Opus frame 2024-12-06 15:27:18 +03:00
Sergey Stepanov
d77d69a331 Remove pool from the audio stretcher 2024-12-05 13:50:39 +03:00
Sergey Stepanov
297ec9005c
Display video scaling info 2024-12-05 01:35:48 +03:00
Sergey Stepanov
5649d4410a
Remove pools from YUV conv 2024-12-05 01:11:02 +03:00
Sergey Stepanov
db32479c4e
Destroy rooms when the coordinator was lost 2024-12-05 01:10:16 +03:00
Sergey Stepanov
8fa53f4e32
Disable macos 2024-12-04 22:16:58 +03:00
Sergey Stepanov
a7acebc5d0
Try YUV without the mem pool 2024-12-04 22:09:51 +03:00
Sergey Stepanov
954bb23bb8
Add Reset with 0 key 2024-12-03 00:38:15 +03:00
Sergey Stepanov
7134782245
Enable frame duplication for Mupen64 2024-12-03 00:34:43 +03:00
Sergey Stepanov
5a42dc9857
Fail x2 on no coordinator connection 2024-12-01 20:26:29 +03:00
Sergey Stepanov
9caf45af78
Reset fail timer on success 2024-12-01 18:20:54 +03:00
Sergey Stepanov
56e3ce328e
Update Go to 1.23.3 2024-11-30 21:35:40 +03:00
Sergey Stepanov
6de1828ffe Wait user click when autoplay fails 2024-11-29 14:51:33 +03:00
Sergey Stepanov
b2e275a6cd
Don't crash the app on http2 garbage 2024-11-28 23:24:35 +03:00
Sergey Stepanov
45dba68b15
Faster CopyFile 2024-11-28 21:16:31 +03:00
Sergey Stepanov
31c670252c Update dependencies 2024-11-26 19:35:31 +03:00
Sergey Stepanov
1831e44eef Add new saveStateFs config param
Used when you need a copy of FS for new game sessions (i.e. DOSBox uniqueSaveDir=true).
2024-11-26 19:35:31 +03:00
Sergey Stepanov
71f5de3bf9 Update dependencies 2024-11-26 19:35:31 +03:00
Sergey Stepanov
88a0911f93
Avoid segfault with nil error handlers in ws 2024-11-17 22:27:34 +03:00
Sergey Stepanov
8686c4a6e5
Return workers by wids right away ;_; 2024-11-17 22:13:39 +03:00
Sergey Stepanov
68acb5d790
Show worker tags in manager 2024-11-17 21:24:08 +03:00
Sergey Stepanov
2c50ae2290
Allow one game per a direct worker 2024-11-17 20:29:45 +03:00
Sergey Stepanov
f09500f289
Check slots for direct workers 2024-11-17 19:55:14 +03:00
Sergey Stepanov
1147aeda14 Track all worker saves to resume old games
Move library config to the top level
2024-11-17 12:59:43 +03:00
Sergey Stepanov
7b57f73b26 Send worker lib 2024-11-17 12:59:43 +03:00
Sergey Stepanov
45cc9e8245 Move library config to the top level 2024-11-17 12:59:43 +03:00
Sergey Stepanov
795771e3d6 Add custom file lock 2024-11-17 12:59:43 +03:00
Sergey Stepanov
2ef1a93eaf
Add playsinline video attribute for Safari 2024-10-23 12:25:29 +03:00
Sergey Stepanov
6ccbea8bd9
Fix test 2024-10-18 22:20:34 +03:00
Sergey Stepanov
003eb5b995
Add CBR mode and max-rate, buf-size config options to x264 2024-10-18 21:59:41 +03:00
Sergey Stepanov
0ab6f58d36
Update dependencies 2024-10-18 21:56:54 +03:00
Sergey Stepanov
763f1e5d11
Update Ubuntu Docker container 2024-09-29 23:32:32 +03:00
Sergey Stepanov
16cf91f669
Update Ubuntu Docker container 2024-09-29 23:06:26 +03:00
Sergey Stepanov
0d8db25c3c
Change the video dimensions when playing 2024-09-28 00:30:50 +03:00
Sergey Stepanov
2084d0958b
Update dependencies 2024-09-24 22:59:11 +03:00
Sergey Stepanov
a67a077024
Fix env functions 2024-09-16 21:45:06 +03:00
Sergey Stepanov
f1ece58c7b
Add new aliasFile option
The option allows changing the alias file name.
2024-09-14 20:38:16 +03:00
Sergey Stepanov
fd34d5a972
Update dependencies 2024-09-14 20:38:11 +03:00
Sergey Stepanov
bdf3598367 Add game aliases
Allows different game names to be set in the alias.txt file [as name=alias] located in the games directory.
2024-08-31 22:29:31 +03:00
Sergey Stepanov
b9d35fa626
Fix save dir string freeing order 2024-08-27 14:30:49 +03:00
Sergey Stepanov
7da993a4c7 Add the uniqueSaveDir option
This option allows for the safe use of distinct filesystem snapshots of games with some cores (e.g., DosBox). Keep in mind that with this option enabled, game changes won't be saved (the unique save folder will be deleted on exit) until you explicitly call the save (or share) function. Thus, you will need files like dosbox.conf along with the games to use some default behaviors with each new game session.
2024-08-21 18:52:26 +03:00
Sergey Stepanov
ddb16f899f
Update dependencies 2024-08-17 18:13:46 +03:00
Sergey Stepanov
dea9926e4f
Update Go version to 1.23.0 2024-08-15 15:44:56 +03:00
Sergey Stepanov
61eb55f736
Update Go version to 1.22.6 2024-08-11 13:38:42 +03:00
Sergey Stepanov
e2521eea94
Update cr docker-compose for X11 2024-08-11 13:04:50 +03:00
Sergey Stepanov
8f859cd600
Reinit encoders with the mutexes 2024-08-11 12:18:41 +03:00
Sergey Stepanov
0232384fe2 Use mutex when switching media encoders 2024-08-11 11:50:54 +03:00
Sergey Stepanov
7873631613
Add skip_same_thread_save hack 2024-08-09 00:54:08 +03:00
Sergey Stepanov
466257d3be
Add NonBlockingSave option for background saving
This feature introduces a new configuration option, `NonBlockingSave`, which allows background saving for large files. With this param the saving process will not block the main thread with all network sockets.
By default, it's enabled for the DosBox core.
2024-08-07 20:31:14 +03:00
Sergey Stepanov
1ff7be38eb
Update x/image 2024-08-06 20:36:15 +03:00
Sergey Stepanov
c87b5cec65
Revert "Update dependencies"
This reverts commit 80afc18892.
2024-08-06 20:34:34 +03:00
Sergey Stepanov
80afc18892
Update dependencies 2024-08-06 20:01:01 +03:00
Sergey Stepanov
34a947ac6d
Update libretro.h 2024-08-06 19:57:45 +03:00
Sergey Stepanov
d855e56a2f Add a DOS game (Rogue) 2024-08-02 11:04:44 +03:00
Sergey Stepanov
7ee98c1b03 Add keyboard and mouse support
Keyboard and mouse controls will now work if you use the kbMouseSupport parameter in the config for Libretro cores. Be aware that capturing mouse and keyboard controls properly is only possible in fullscreen mode.

Note: In the case of DOSBox, a virtual filesystem handler is not yet implemented, thus each game state will be shared between all rooms (DOS game instances) of CloudRetro.
2024-08-02 11:04:44 +03:00
Sergey Stepanov
af8569a605
Update dependencies 2024-07-26 17:43:33 +03:00
sergystepanov
d6199c9598
Use ROM folders
Libretro cores are selected based on the file extensions of the ROMs. However, ROM file extensions are not unique across cores (e.g., .zip is used in both DosBox and MAME). To load a specific core correctly, it's necessary to place the corresponding ROMs in designated folders specified in the configuration. By default, you can use the keys from cores.list as the folder names, or you can specify your own custom folder names using the 'folder' parameter for each core.
2024-07-24 20:31:12 +03:00
Sergey Stepanov
ba7db72093
Increase buffers that use max dimensions from the Libretro geometry
Some cores may render frames bigger that reported max dimensions. This may help a bit.
2024-07-15 12:10:18 +03:00
Sergey Stepanov
83b040b39f
Change GL framebuffer if max geometry is different 2024-07-14 19:33:32 +03:00
Sergey Stepanov
d8a3e82f1e
Update dependencies 2024-07-14 19:31:44 +03:00
Sergey Stepanov
c40f9c9127 Resize GL buffer when geometry changes 2024-07-13 11:28:20 +03:00
sergystepanov
7e612458a0
Fix crash when loading games too early (also for mac) 2024-07-10 00:36:20 +03:00
Sergey Stepanov
72b791cc5e
Revert shutdown locks for now (deadlock on macOS) 2024-07-09 23:17:44 +03:00
Sergey Stepanov
e46b739311
Fix crash when loading games too early 2024-07-09 21:21:08 +03:00
Sergey Stepanov
daf6a20e1d
Update dependencies 2024-07-09 21:19:41 +03:00
sergystepanov
ba45936d77
Add another workaround for g0 stack in Go 1.22.5 (#460) 2024-07-05 22:56:28 +03:00
sergystepanov
ca64bd127e
Remove useless code from the libyuv wrapper 2024-05-26 02:34:00 +03:00
Sergey Stepanov
b93eb4911c
Fix conf loading for the emulator mocks 2024-05-21 22:26:20 +03:00
Sergey Stepanov
20e9449bb1
Fix for build.yml 2024-05-21 20:20:21 +03:00
sergystepanov
c2e9d67bcb
Update Windows software OpenGL Mesa drivers to 24.0.7 (#454)
Needed for Zink (Vulkan OpenGL) in the future.
2024-05-21 20:14:52 +03:00
Sergey Stepanov
3989a735ac
Update dependencies 2024-05-20 22:46:59 +03:00
Sergey Stepanov
ede15c4fe5
Make x264 glue code not mem-pinned 2024-05-19 18:55:37 +03:00
Sergey Stepanov
99976dd560
Add frame-options option 2024-05-13 19:29:13 +03:00
Sergey Stepanov
0500550fc0
Cleanup 2024-05-07 23:06:09 +03:00
Sergey Stepanov
c5c2578d0f
Switch Trace to Debug in Libretro logging 2024-05-07 23:05:03 +03:00
Sergey Stepanov
b843538fea
Update dependencies 2024-05-07 21:08:35 +03:00
Sergey Stepanov
9b56ffc87c
Fix macOS tests
Main thread locking hangs OpenGL emulators.
2024-05-07 21:05:12 +03:00
Sergey Stepanov
a4f0dbbca8
Add a health check in cloudretro.io 2024-05-07 19:11:55 +03:00
Sergey Stepanov
b530f7a6cf
Add curl to worker containers 2024-05-07 15:54:42 +03:00
sergystepanov
421e9115cc
Add new options4rom config param (#452)
* Add new options4rom config param

Allows changing core options depending on the ROM name.
2024-04-24 16:38:33 +03:00
Sergey Stepanov
b812887f6e
LibJPEG linking is broken on macOS 14 Sonoma :/ 2024-04-23 21:09:48 +03:00
Sergey Stepanov
b3f677d32f Use for range 2024-04-22 18:10:35 +03:00
Sergey Stepanov
b755bcd1bf Use <video> play listener instead of canplay 2024-04-22 18:10:35 +03:00
Sergey Stepanov
8d79680b81 Migrate to rand/v2 2024-04-22 18:10:35 +03:00
Sergey Stepanov
a013192bf8 Clean webrtc.js 2024-04-22 11:47:46 +03:00
Sergey Stepanov
dceb6f9993 Reuse retropad poll fn 2024-04-22 11:45:07 +03:00
Sergey Stepanov
d922e58278 Fix UI 2024-04-22 11:43:38 +03:00
Sergey Stepanov
8caad44ade Update dependencies 2024-04-22 11:34:56 +03:00
Sergey Stepanov
22d1bd7620 Add screen component 2024-04-07 00:20:47 +03:00
Sergey Stepanov
effa5c46c5
Update UA/PLT detection 2024-04-03 19:52:42 +03:00
Sergey Stepanov
cebbcdf256 Refactor WebRTC stats 2024-04-02 21:10:05 +03:00
Sergey Stepanov
f557d16997
Fix broken link 2024-03-31 22:08:32 +03:00
Sergey Stepanov
3e0fcfbfcf
Enable SCTP zero checksums 2024-03-31 21:31:18 +03:00
Sergey Stepanov
7377b4f15b
Update dependencies 2024-03-31 21:30:46 +03:00
Sergey Stepanov
ecbe7f6ad9
Remove unused ping stats module 2024-03-31 21:22:41 +03:00
Sergey Stepanov
084c14175e
Use AR correction in MAME 2024-03-22 00:17:22 +03:00
Sergey Stepanov
5da77a6b4f
Fix aspect ratio of PSX games in full-screen 2024-03-21 23:02:53 +03:00
Sergey Stepanov
84f55691eb
Check if dup frame didn't exist
FBNeo can return dup frame flag before its first frame.
2024-03-21 23:02:02 +03:00
Sergey Stepanov
4d5033f03c Allow duplicate frames
Some cores for performance reasons may return duplicate frames (i.e. previous frames) instead of rendering them again.
2024-03-21 16:10:09 +03:00
Sergey Stepanov
ff6c344a15 Update dependencies 2024-03-21 16:10:09 +03:00
Sergey Stepanov
104498dec0
Fix wrong import order of some modules 2024-03-18 13:45:01 +03:00
Sergey Stepanov
8654604b9b Fix index.html warnings 2024-03-17 22:09:43 +03:00
Sergey Stepanov
2bc64a3be8 Migrate from IIFE to modern ES modules
These modules should be supported by all contemporary browsers, and this transition should resolve most issues related to the explicit import order of the .js files.
2024-03-17 22:09:43 +03:00
Sergey Stepanov
2aaf37b766 Add Cache-Control for serving static files
Static files will be rechecked every 3 days instead of unlimited cache time. The Cache-Control header is mandatory in order to make browsers handle cache properly with Go's FileServer. The option can be modified in the server.CacheControl line of the config file.
2024-03-17 22:09:43 +03:00
Sergey Stepanov
47bd72e1cd
Fix broken options button 2024-03-16 13:15:05 +03:00
Sergey Stepanov
29eedee3ec
Fix keybindings for options 2024-03-15 14:38:30 +03:00
Sergey Stepanov
a349fdd0cf Add 'force full-screen' option 2024-03-14 12:24:39 +03:00
Sergey Stepanov
cf5248ec54 Fix missing gameList transition handler 2024-03-14 12:24:39 +03:00
Sergey Stepanov
4fc53e7220 Update options UI 2024-03-14 12:24:39 +03:00
Sergey Stepanov
f8392ab0be
Update dependencies 2024-03-08 18:46:46 +03:00
Sergey Stepanov
43d3f84993
Add support of the 0RGB1555 pixel format 2024-03-08 18:43:23 +03:00
Sergey Stepanov
72e846894e
Use case-insensitive sort for games 2024-03-07 23:18:42 +03:00
Sergey Stepanov
84d2261391
Don't stretch portrait games 2024-03-07 17:25:26 +03:00
Sergey Stepanov
608da9f64b
Track fullscreen for <video> 2024-03-05 22:07:12 +03:00
Sergey Stepanov
91ace06f8b
Replace the hasMultitap option with a more general solution
The new hid option enables users to map a specific Libretro device (or multiple devices) to the input ports. For instance, this allows users to map a Multitap controller with the snes9x core.
2024-03-05 21:34:37 +03:00
Sergey Stepanov
cdbb5e98f5
Clean 2024-03-02 16:46:08 +03:00
Sergey Stepanov
92e59672f9
Expose scale factor value 2024-03-02 16:38:53 +03:00
Sergey Stepanov
3568b7a12a
Update dependencies 2024-03-02 16:23:11 +03:00
Sergey Stepanov
4195b7f2dc
Disable load test for new mGBA 2024-02-29 00:53:04 +03:00
Sergey Stepanov
17fe1a938a
Fix serialize test for new mGBA
Consecutive retro_serialize calls won't return same states for mGBA anymore.
2024-02-29 00:38:41 +03:00
Sergey Stepanov
c699455b58
Hide video element controls in fullscreen 2024-02-25 12:51:37 +03:00
Sergey Stepanov
000bc4f661
Load apps after rendering 1 frame
This is mandatory for Mupen and DOSBox save states. Enabled for all emulators.
2024-02-25 12:33:03 +03:00
Sergey Stepanov
9308e1b388
Sort lib alphabetically in console 2024-02-20 21:59:36 +03:00
Sergey Stepanov
1452317d45
Scan ROM extensions case-insensitive 2024-02-20 21:39:49 +03:00
Sergey Stepanov
41bfe4f4d3
Fix WebRTC datachannels in FF 2024-02-20 21:29:57 +03:00
Sergey Stepanov
b79b4c405a
Get random free port in websocket tests 2024-02-17 21:25:14 +03:00
Sergey Stepanov
e7e281083f
Add ugly persistent volume option 2024-02-16 22:47:54 +03:00
Sergey Stepanov
3459c7e8d6 Add VP9 encoder option 2024-02-15 14:06:28 +03:00
Sergey Stepanov
6258f9a5e4
Add RTCP packet reader for output streams
Default interceptors need those.
2024-02-13 21:17:27 +03:00
Sergey Stepanov
61b4108dce
Disable save states tests for Nestopia
Savestates are broken in the Nestopia version 1ae59e3. Wait when new version (revert) is pushed into the nightly repo.
2024-02-13 19:37:23 +03:00
Sergey Stepanov
ce7aa1be62
Disable frame duplication by default
It breaks newer PCSX rearmed versions by pushing dozen of frames in bursts.

To implement a proper support later.
2024-02-13 18:50:38 +03:00
Sergey Stepanov
e2226e7492
Remove Encoder.LoadBuf interface method
There is no point in keeping it only for early YUV image pooling.
2024-02-12 11:23:52 +03:00
Sergey Stepanov
b903700077
Update dependencies 2024-02-11 15:30:45 +03:00
Sergey Stepanov
46067dec8f
Use half GOP size in h264 2024-02-11 15:01:27 +03:00
Sergey Stepanov
11295a28f6 Fix h264 lib for Go 1.22
Unwrapped X264_ C structs prevent crash on Go version 1.22.
2024-02-10 21:13:49 +03:00
Sergey Stepanov
4e241d0448 Swap x264-git dep to libx264 in Msys2/Arch 2024-02-10 21:13:49 +03:00
Sergey Stepanov
a6e56a208c Pin nano string options during Retro callbacks 2024-02-10 21:13:49 +03:00
Sergey Stepanov
ccb0f410ab Revert "Revert Go version back to 1.20"
This reverts commit 1a44b94c85.
2024-02-10 21:13:49 +03:00
Sergey Stepanov
1a44b94c85
Revert Go version back to 1.20
Go 1.22 crashes under Windows with h264 encoder.
2024-02-08 16:38:40 +03:00
Sergey Stepanov
53e55728db
Add PGO for 1.22 2024-02-07 15:15:41 +03:00
Sergey Stepanov
53a3624aef
Upload test frames separately 2024-02-07 12:48:59 +03:00
Sergey Stepanov
d6ceaad220
Enable overwrite for the upload-artifact action 2024-02-07 12:39:17 +03:00
Sergey Stepanov
e67b98d6fe
Update Github actions 2024-02-07 12:33:29 +03:00
Sergey Stepanov
a3f07057f4
Update dependencies 2024-02-07 12:23:14 +03:00
Sergey Stepanov
610e087bcd Update to Go 1.22.0 2024-02-07 11:55:40 +03:00
Sergey Stepanov
fca46f1a32 Update dependencies 2023-12-22 20:49:16 +03:00
sergystepanov
f7d12e65e5
Update README.md
Add some additional libyuv notes.
2023-12-22 16:09:59 +03:00
sergystepanov
27c9ce681b
Update README.md 2023-12-04 18:39:26 +03:00
Sergey Stepanov
1993950cd7
Downgrade requirements for an old Ubuntu 2023-12-04 18:20:05 +03:00
Sergey Stepanov
f475dbabb7
Update dependencies 2023-12-04 01:38:30 +03:00
Sergey Stepanov
b7b530fe60
Update libretro.h 2023-11-26 22:39:52 +03:00
Sergey Stepanov
4fbfa1d4e3
Fix possible NPEs 2023-11-26 22:39:46 +03:00
Sergey Stepanov
a77069a634
Add L2, R2, L3, R3 mappings to keyboard 2023-11-26 22:39:43 +03:00
Sergey Stepanov
aa10008d1b
Use default Pion webrtc interceptors
Slightly higher latency, but more stable in high ping situations.
2023-11-16 01:16:09 +03:00
Sergey Stepanov
e6e537d799 Add a generic S3 provider for cloud saves 2023-11-13 21:33:12 +03:00
Sergey Stepanov
2e91feb861 Add initial automatic aspect ratio change
Depending on the configuration param coreAspectRatio, video streams may have automatic aspect ratio correction in the browser with the value provided by the cores themselves.
2023-11-03 01:12:22 +03:00
Sergey Stepanov
d805ba8eb8 Update dependencies 2023-11-03 01:12:22 +03:00
Himaj333
ad07ad2014 Fixed README.md 2023-10-26 16:57:19 +03:00
Sergey Stepanov
5d65ff14d5
Fix test 2023-10-26 16:39:00 +03:00
guangwu
99ceb5d72c fix: typo 2023-10-26 16:34:51 +03:00
sergystepanov
7f2f1d70b1
Add configurable debouncer for spammy Libretro callbacks 2023-10-25 21:41:36 +03:00
Sergey Stepanov
07f40351fa
Show PCSX boot logo by default 2023-10-24 12:55:39 +03:00
Sergey Stepanov
6525106116
Update video encoders 2023-10-24 12:55:27 +03:00
Sergey Stepanov
3e116fcc52
Drop users when coordinator is lost 2023-10-21 18:45:38 +03:00
Sergey Stepanov
cb968d782a
Show rooms in the list 2023-10-21 02:37:44 +03:00
Sergey Stepanov
10507d9c53 Reorder shutdown functions 2023-10-21 00:34:15 +03:00
Sergey Stepanov
10c4cd9b7f Add start/stop frontend lock 2023-10-20 22:43:51 +03:00
Sergey Stepanov
38dc69e4a2 Show hanged rooms 2023-10-20 22:43:51 +03:00
Sergey Stepanov
377306dc80 Clean frontend tests 2023-10-20 22:43:51 +03:00
Sergey Stepanov
da7059dc79 Tame logs 2023-10-20 22:43:51 +03:00
Sergey Stepanov
9ec6541322 Update dependencies 2023-10-20 22:43:51 +03:00
Sergey Stepanov
e80e31da42 Use low pass filter with GBA 2023-10-20 22:43:51 +03:00
Sergey Stepanov
a69a934029 Remove unused csp param 2023-10-20 22:43:51 +03:00
Sergey Stepanov
e4aab1019c Don't copy YUV planes in x264 2023-10-19 17:28:45 +03:00
Sergey Stepanov
fb5d8c216b
Don't change players when there is no game 2023-10-18 21:46:33 +03:00
Sergey Stepanov
7977bce8a3
Fix dangling rooms when multiplaying 2023-10-18 21:29:21 +03:00
Sergey Stepanov
a8d47fd1bf
Don't nil peerconnection while receiving ICE 2023-10-18 20:42:46 +03:00
Sergey Stepanov
1b82c48dc1
Don't show the share popup message 2023-10-18 20:37:57 +03:00
Sergey Stepanov
f7c2524098
Add libjpeg to builds 2023-10-18 14:24:47 +03:00
Sergey Stepanov
61c70d3289
Use static CC build 2023-10-18 13:53:35 +03:00
Sergey Stepanov
494ac0ed3b
Add empty rooms watcher 2023-10-17 21:39:16 +03:00
Sergey Stepanov
9c768277c7 Use sudo static Docker builds 2023-10-17 19:23:00 +03:00
Sergey Stepanov
e8cec39476 Report on broken config when downloading Libretro cores 2023-10-17 19:23:00 +03:00
Sergey Stepanov
afb76aa970 Add note about empty nested config values 2023-10-17 19:23:00 +03:00
Sergey Stepanov
d698660c19 Allow changing video resolution during the stream 2023-10-17 15:16:45 +03:00
Sergey Stepanov
f8fb128e97 Update CI 2023-10-16 14:57:25 +03:00
Sergey Stepanov
f11cad157b Use static libyuv for macs 2023-10-16 01:50:06 +03:00
Sergey Stepanov
b1b33713d6 Add the initial libyuv support
The main benefit of libyuv, apart from shortening the video pipeline, is quite noticeable latency and CPU usage decrease due to various assembler/SIMD optimizations of the library. However, there is a drawback for macOS systems: libyuv cannot be downloaded as a compiled library and can only be built from the source, which means we should include a cropped source code of the library (~10K LoC) into the app or rise the complexity of macOS dev and run toolchains. The main target system -- Linux, and Windows will use compiled lib from the package managers and macOS will use the lib included as a shortened source-code.

Building the app with the no_libyuv tag will force it to use libyuv from the provided source files.
2023-10-15 18:55:53 +03:00
Sergey Stepanov
072b674fb1 Clean room init/deinit handlers 2023-10-15 18:55:53 +03:00
Sergey Stepanov
989d3b1c85 Move encoding libs to the top package level 2023-10-15 18:55:53 +03:00
Sergey Stepanov
92aea18a8c
Update dependencies 2023-10-13 11:13:27 +03:00
Sergey Stepanov
f875d3fc42
Guard room creation with a mutex
Possible fix for the concurrent room creation while other is not destroyed properly.
2023-10-06 13:30:10 +03:00
Sergey Stepanov
cddf081b8f
Use locks in router with rooms 2023-10-06 01:12:27 +03:00
Sergey Stepanov
c1c4731640 Use faster Y flip of the video encoders (OpenGL/N64) 2023-09-29 22:36:14 +03:00
Sergey Stepanov
a901c84d99
Update dependencies 2023-09-26 21:01:19 +03:00
Sergey Stepanov
226bb0384e Remove Quit notification 2023-09-24 14:22:34 +03:00
Sergey Stepanov
8703309090 Remove old ping/pong handlers 2023-09-24 14:22:34 +03:00
Sergey Stepanov
da51639625 Clean canvas.c 2023-09-24 14:22:34 +03:00
Sergey Stepanov
e83614068f MacOS fix 2023-09-23 20:43:46 +03:00
Sergey Stepanov
56d8c4a928
Clean canvas drawing functions 2023-09-23 20:27:12 +03:00
Sergey Stepanov
85cef0dfec Convert colors in C 2023-09-22 18:46:08 +03:00
Sergey Stepanov
8dd9e9c9be Use buffered file writer 2023-09-22 18:46:08 +03:00
Sergey Stepanov
196930281b Add the initial caged apps abstraction
In the current version of the application, we have strictly hardcoded the captured runtime application (FFI Libretro frontend) as well as the streaming transport (WebRTC). This commit makes it possible to choose these components at runtime.

In this commit, we no longer manage initially connected users separately from the rooms, and instead, we treat all users as abstract app sessions, rather than hardcoded WebRTC connections. These sessions may contain all the transport specifics, such as WebRTC and so on.

Rooms, instead of having the hardcoded emulator app and WebRTC media encoders, now have these components decoupled. In theory, it is possible to add new transports (e.g., WebTransport) and streaming apps (e.g., wrapped into an ffmpeg desktop app).
2023-09-16 20:12:24 +03:00
Sergey Stepanov
878d7fe298 API cleanup 2023-09-16 20:12:24 +03:00
Sergey Stepanov
cccb3dce84 Fix some test on Arch 2023-09-16 20:12:24 +03:00
Sergey Stepanov
992b6e06da Hide sys info in the frontend 2023-09-16 20:12:24 +03:00
Sergey Stepanov
d7e7112e25 Update dependencies 2023-09-16 20:12:24 +03:00
Sergey Stepanov
594b02d37e Avoid exe/non-exe conflicts with WSL2 when dev.run is called 2023-09-16 20:12:24 +03:00
Sergey Stepanov
3b06905e15 Skip peer connection state error due to DTLS spam 2023-09-16 20:12:24 +03:00
Sergey Stepanov
f2d21c67dc
Partial fix for convoluted game list ext info bug 2023-09-01 22:55:18 +03:00
Sergey Stepanov
ae3f91dfc6
Add diagnostic input to FBNeo on crio 2023-08-24 10:16:58 +03:00
Sergey Stepanov
882aae9daf
Use Go 1.20.7 for builds 2023-08-24 00:37:56 +03:00
Sergey Stepanov
c27c88c0fa
Disable canvas pool with recording enabled for now 2023-08-24 00:14:25 +03:00
Sergey Stepanov
240a1f92ce
Use Go 1.20.7 with Docker builds 2023-08-09 19:55:44 +03:00
Sergey Stepanov
8e92f6822e
Revert Go version to 1.20 due to MacOS crash 2023-08-09 19:19:10 +03:00
Sergey Stepanov
b2e4848ed3
Update to Go 1.21.0 2023-08-09 19:08:25 +03:00
Sergey Stepanov
ddc841e8c6
Fold vpx/h264 struct options 2023-08-01 22:36:25 +03:00
Sergey Stepanov
49cb752b5c
Use implicit cast of app/game structs
That will make the code a bit tidier.
2023-08-01 22:27:21 +03:00
Sergey Stepanov
7fe3a893f6
Remove service package 2023-08-01 22:27:08 +03:00
Sergey Stepanov
7c0a2051d4
Disable mDNS for ICE 2023-08-01 22:26:37 +03:00
Sergey Stepanov
b7fb079243
Derace the frontend when parallel testing 2023-08-01 22:25:55 +03:00
Sergey Stepanov
f9a5ce0e68
Make embedded conf reentrant 2023-08-01 22:25:43 +03:00
Sergey Stepanov
d278ab6e3d
Remove cache when loading YAML configs for mem 2023-08-01 22:22:32 +03:00
Sergey Stepanov
4a627a30f2
Use alt pcsx-rearmed build for prod 2023-07-08 23:07:38 +03:00
Sergey Stepanov
75bb90c90d
Enable PGO 2023-07-07 21:27:33 +03:00
Sergey Stepanov
a0e549a6b9
Add worker PGO 070723 2023-07-07 16:19:30 +03:00
Sergey Stepanov
d7e8ca5ace
Remove SSL from the monitoring routes 2023-07-07 15:28:50 +03:00
Sergey Stepanov
be83b1c287
Skip HTTPS redirect for monitoring 2023-07-06 15:31:55 +03:00
Sergey Stepanov
8fef57aa8d
Use TLS with the monitoring / profiling routes 2023-07-06 00:04:05 +03:00
Sergey Stepanov
cc414881ea
Update dependencies 2023-07-06 00:01:27 +03:00
Sergey Stepanov
65b6e3208f
Fix lint warnings 2023-06-27 23:45:57 +03:00
Sergey Stepanov
d5bb271469
Add core hacks options 2023-06-23 18:39:36 +03:00
Sergey Stepanov
c063dd92c6 Remove unused CSS rules 2023-06-16 14:12:41 +03:00
Sergey Stepanov
42b82a368c Update deps 2023-06-16 14:12:41 +03:00
Sergey Stepanov
6106eee97e Update game list module 2023-06-16 14:12:41 +03:00
Sergey Stepanov
2e1c837643 Show systems in the interface 2023-06-16 14:12:41 +03:00
Sergey Stepanov
860c8b9d45 Add system param into the library 2023-06-16 14:12:41 +03:00
Sergey Stepanov
1f7b5139c6 Remove session from lib 2023-06-16 14:12:41 +03:00
Sergey Stepanov
e5ac43a59e Use faster lib scan 2023-06-16 14:12:41 +03:00
Sergey Stepanov
a6bcf5cd94
Fix scale option (it is slow) 2023-05-27 17:36:57 +03:00
sergystepanov
7668ef7bd8
Refactor media (#401)
* Encapsulate media
* Write audio by 4 bytes instead 2
* Update deps
2023-05-27 17:34:35 +03:00
Sergey Stepanov
2ed6e8724f
Add Alwa's Awakening (Demo) NES ROM 2023-05-22 18:34:58 +03:00
Sergey Stepanov
a4f47396e5
Remove unused 404 handler 2023-05-21 18:45:46 +03:00
Sergey Stepanov
d237a5b6ea
Update dependencies 2023-05-21 13:59:33 +03:00
Sergey Stepanov
5b4f74e2b7
Notify users when there are no gaming slots 2023-05-21 13:54:21 +03:00
Sergey Stepanov
624eecd4e8
Show errors when ICE fails 2023-05-17 08:57:16 +03:00
Sergey Stepanov
851d9a6fc1
Use new Docker compose in Makefile 2023-05-17 08:56:13 +03:00
Sergey Stepanov
167071af6f
Update Dockerfile 2023-05-16 14:53:20 +03:00
Sergey Stepanov
75e41e4fd0
Fix static x264 static link on Windows 2023-05-14 01:47:58 +03:00
Sergey Stepanov
0aeaa5580c
Don't scan the lib with errored dirs 2023-05-14 01:47:02 +03:00
Sergey Stepanov
70d9ff32b7
Update dependencies 2023-05-12 21:21:51 +03:00
Sergey Stepanov
d985440930
Center menu item 2023-05-12 21:20:08 +03:00
Sergey Stepanov
39c63ec44a
Update cloudretro.io deployments 2023-05-12 16:49:15 +03:00
Sergey Stepanov
1dc0cabc2b Add special Dockerfile for tiny coordinator (<10Mib) and worker (<150Mib) containers 2023-05-12 14:31:21 +03:00
Sergey Stepanov
b227260060 Embed config.yaml into both apps 2023-05-12 14:31:21 +03:00
Sergey Stepanov
9231120a55 Add base path for games in workers 2023-05-12 14:31:21 +03:00
Sergey Stepanov
63e3a7f6bd
Read YAML keys in lowercase internally 2023-05-10 22:22:31 +03:00
Sergey Stepanov
3815e18027
Remove the hack involving the /static route 2023-05-03 11:11:08 +03:00
Sergey Stepanov
73639eeb64
Clean docs 2023-05-02 11:25:51 +03:00
sergystepanov
3175747ee5
Update README.md 2023-05-02 00:20:07 +03:00
Sergey Stepanov
2add701c39
Update game picker interface 2023-04-27 19:50:07 +03:00
Sergey Stepanov
f7d8a5dc50
Revert opts z-index 2023-04-27 16:27:05 +03:00
Sergey Stepanov
ed21e32b1f
Speedup item pick 2023-04-27 16:15:09 +03:00
Sergey Stepanov
bee6192894
Use old font 2023-04-27 15:15:37 +03:00
Sergey Stepanov
e50dc102bb
Make game pick time consistent 2023-04-27 14:49:54 +03:00
Sergey Stepanov
30584b3faa
Make game picking less ugly 2023-04-27 14:26:16 +03:00
Sergey Stepanov
dc48c93804
Hide load/save by default 2023-04-27 13:33:07 +03:00
Sergey Stepanov
0c532a7f93
Fix moving settings 2023-04-27 13:31:17 +03:00
Sergey Stepanov
c4895fae84
remove unselect from js menu items 2023-04-27 13:27:34 +03:00
Sergey Stepanov
e17a358ade
Fix circle pad layering text blur 2023-04-27 13:06:43 +03:00
Sergey Stepanov
bb9f904104
Fix menu select side blur 2023-04-27 13:05:12 +03:00
Sergey Stepanov
40bd57c9a0
Remove not needed browser prefixes 2023-04-27 12:28:28 +03:00
Sergey Stepanov
ea3263ca6c
Remove unselected 2023-04-27 12:24:07 +03:00
Sergey Stepanov
7455ad3f47
Update deployment to cloudretro.io 2023-04-26 23:27:00 +03:00
Sergey Stepanov
317079ddd3
Merge remote-tracking branch 'origin/master' 2023-04-26 23:10:52 +03:00
Sergey Stepanov
d9def73096
Add ca-certificates to the images 2023-04-26 23:10:35 +03:00
Sergey Stepanov
e485cc8862
Add ca-certificates to the images 2023-04-26 23:07:13 +03:00
Sergey Stepanov
b6d6e464ed
Optimize images 2023-04-26 20:11:08 +03:00
Sergey Stepanov
494979cf38
Optimize images 2023-04-26 20:08:20 +03:00
Sergey Stepanov
229cd4044c
Fix broken warning in the wrtc js module 2023-04-24 14:16:25 +03:00
Sergey Stepanov
62fc68e88b
Add optional static linking with vpx, x264, opus 2023-04-24 00:16:00 +03:00
Sergey Stepanov
ece1efad16
Add optional static linking with vpx, x264, opus 2023-04-23 23:51:38 +03:00
Sergey Stepanov
9bb0c151a9
Compress apps for Docker 2023-04-23 20:49:29 +03:00
Sergey Stepanov
3bd32715cb
Remove unused fonts 2023-04-23 19:55:39 +03:00
Sergey Stepanov
e10490adda
Remove ca-certificates from the Docker image 2023-04-23 13:41:40 +03:00
Sergey Stepanov
8893e1e5bf Update config manager 2023-04-22 17:05:28 +03:00
Sergey Stepanov
c13099c56a Remove hackish way of adding ICE servers
Use config files instead (k8s ConfigMap or something).
2023-04-22 17:05:28 +03:00
Sergey Stepanov
eda90d74e6
Rename C.stop (it crashes PCSX on Ubuntu) wtf omegalol 2023-04-21 00:26:41 +03:00
Sergey Stepanov
ee58af488d
Advance 1 frame only with Mupen 2023-04-20 22:54:35 +03:00
Sergey Stepanov
c8eb672ecd
Fix missing return 2023-04-20 22:54:12 +03:00
Sergey Stepanov
b65e8dc3f3
Use master for deploy 2023-04-20 22:53:44 +03:00
Sergey Stepanov
137d1b63d8
Fix build-args for Docker GHA 2023-04-20 21:59:52 +03:00
Sergey Stepanov
31638b0805
Update Docker GH Action
Now it will push the master tag instead of dev for the main branch.
2023-04-20 21:49:52 +03:00
Sergey Stepanov
df980c5cf4
Update versions 2023-04-20 14:19:40 +03:00
Sergey Stepanov
f00c27a226
Clean nanoarch 2023-04-20 14:18:57 +03:00
Sergey Stepanov
853c7a6f81 Fix save/load for Mupen64
By passing function arguments into the synchronized un-libco loop, we can now save and restore Mupen game states without segfaults.
2023-04-16 13:24:28 +03:00
Sergey Stepanov
fbdb517d24 Use latest Mupen core as default for every OS 2023-04-16 13:24:28 +03:00
Sergey Stepanov
b48a6c92f6 Refactor nanoarch names 2023-04-16 13:24:28 +03:00
Sergey Stepanov
5034f86667 Save/Load without malloc 2023-04-16 13:24:28 +03:00
Sergey Stepanov
d830821ceb Move last frame time init fn to the end 2023-04-16 13:24:28 +03:00
Sergey Stepanov
137397dbc1 Update Libretro env handlers 2023-04-16 13:24:28 +03:00
Sergey Stepanov
66dbdba863 Use explicit Libretro log levels instead of magic numbers 2023-04-16 13:24:28 +03:00
Sergey Stepanov
33099d098f Use new page-segmentation friendly unsafe.Slice for mem restore 2023-04-16 13:24:28 +03:00
Sergey Stepanov
43cb05e088 Remove unused OS signal handlers in C
The idea is that we make just one OS signals handler on the C (CGO) side instead of Go and handle everything there from both, which is needed for proper termination process of multithreaded Libretro cores. But it leads to strange segfaults.
2023-04-16 13:24:28 +03:00
Sergey Stepanov
a1b24b0a85 Handle faster most called Libretro env getters 2023-04-16 13:24:28 +03:00
Sergey Stepanov
7c2b88716b Update libretro.h 2023-04-16 13:24:28 +03:00
Sergey Stepanov
9325fddc1f Make RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT return RETRO_SAVESTATE_CONTEXT_NORMAL explicitly for NeoGEO 2023-04-16 13:24:28 +03:00
Sergey Stepanov
13e49a8c3f
Force Mupen A-Stick sensitivity option (implicit default doesn't work) 2023-04-11 01:29:41 +03:00
Sergey Stepanov
5920af5a36
Add RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT for NeoGEO 2023-04-08 01:18:41 +03:00
Sergey Stepanov
256896df52
Try new Ubuntu Lunar as a base Docker image 2023-04-07 22:25:41 +03:00
Sergey Stepanov
f6e86a465f
Increase ws write timeout for slow IO 2023-04-07 22:24:35 +03:00
Sergey Stepanov
7b0d212fc0
Simplify core options passing 2023-04-06 20:40:58 +03:00
Sergey Stepanov
6539ddd09c
Remove not needed NES options 2023-04-06 20:40:14 +03:00
Sergey Stepanov
e3ecd8fef0
Remove not needed rand seeding 2023-04-06 20:39:41 +03:00
Sergey Stepanov
b0b7966b47
Bump minimum Go version to 1.20 2023-04-06 20:37:56 +03:00
Sergey Stepanov
ec5bcbc9c7
Update deployment 2023-04-06 12:59:48 +03:00
Sergey Stepanov
686f5e7e87
Allow override config from /home 2023-04-06 12:01:49 +03:00
sergystepanov
cfd5b1ae8d
Libretro cores config in yaml (#392)
Removes separate config files for Libretro cores stored in the cores folder and replaces them with options in the main config.yaml file.
2023-04-06 11:25:49 +03:00
Sergey Stepanov
7667948bf5
Use old Mupen64 version in Windows by default 2023-04-04 18:49:47 +03:00
Sergey Stepanov
b5cb33812b
Add dummy CLEAR_ALL_THREAD_WAITS_CB for Mupen64 core in order to support the threaded renderer 2023-04-04 17:59:24 +03:00
Sergey Stepanov
9178dab7dd
Fix N64 overlapping frame draw 2023-04-03 20:39:49 +03:00
Sergey Stepanov
583e4659ad
Update deployment 2023-04-03 00:13:37 +03:00
Sergey Stepanov
c797365641
Fix Dockerfile 2023-04-02 22:32:14 +03:00
Sergey Stepanov
99687a1c28
Add precreated dirs to Dockerfile 2023-04-02 22:12:55 +03:00
Sergey Stepanov
0f66f35673
Add the initial RETRO_ENVIRONMENT_SET_MESSAGE support 2023-03-31 18:50:42 +03:00
Sergey Stepanov
40c8867e44
Update docker-compose for deployments 2023-03-30 18:27:56 +03:00
Sergey Stepanov
d7f61c4b30
Update the default deployments 2023-03-29 20:53:16 +03:00
Sergey Stepanov
c6d35cba5e
Set 4 thread for the emulator.
0 threads block the encoder (fixme).
2023-03-29 20:52:45 +03:00
Sergey Stepanov
361c3a67ee
Use builtin Docker Compose in the deployment script 2023-03-29 14:04:38 +03:00
Sergey Stepanov
83b40e10e5
Increase default socket timeouts 2023-03-29 13:37:01 +03:00
Sergey Stepanov
3c2c24038d
Hack 2023-03-29 13:03:31 +03:00
Sergey Stepanov
4a686e9fce
Use single server deploy by default 2023-03-27 22:20:14 +03:00
Sergey Stepanov
9c65d28933
Use sequential image processing for now 2023-03-26 00:20:09 +03:00
Sergey Stepanov
b24e82c145
Fix disabled cloud save downloads 2023-03-26 00:14:19 +03:00
Sergey Stepanov
f30e017101
Disable Oracle provider for the default deployment 2023-03-25 23:59:40 +03:00
Sergey Stepanov
b6366e7b88
Update room tests 2023-03-18 23:50:05 +03:00
sergystepanov
3bb54fdad4
Clean API (#391)
Remove hard coupling between api (all the API data structures) and com (app clients communication protocol and logic).
2023-03-18 20:24:06 +03:00
Sergey Stepanov
c8f5bdbcdb
Update dependencies 2023-03-16 23:49:40 +03:00
Sergey Stepanov
cd056ee976
Bump to v3 2023-03-16 23:46:53 +03:00
Sergey Stepanov
faf347a44a
Make rand.Seed not deprecated 2023-02-17 18:12:38 +03:00
Sergey Stepanov
c237ee1a47
Update dependencies 2023-02-17 17:56:32 +03:00
Sergey Stepanov
04d9817b94
Set worker UID properly 2023-02-14 23:16:15 +03:00
Sergey Stepanov
207d514e8a
Move balancer into hub 2023-02-11 02:16:48 +03:00
Sergey Stepanov
d27d819821
Add variable frame rate (VFR) option for cores.
It is enabled only for N64.
Disabling it for everything else may lead to a much smoother video playback (less stuttering).
2023-02-10 18:03:50 +03:00
Sergey Stepanov
beaf862dec
Don't cache nil frames 2023-02-10 17:47:58 +03:00
Sergey Stepanov
8b4b238cf9
Update dependencies 2023-02-10 14:20:02 +03:00
Sergey Stepanov
3076be593e
Fix possibility of lost IDs for WS requests/responses 2023-02-09 14:37:41 +03:00
Sergey Stepanov
21a2680027
Fix websocket tests 2023-02-09 14:35:56 +03:00
Sergey Stepanov
8006939d69
Update versions 2023-02-04 22:54:18 +03:00
sergystepanov
c7b5d1afc3
Fix possible infinite loop on Mupen close (#389)
For some reason the Libretro Mupen core breaks the pseudo-random order of channel selection leading to constant skip of the done channel on ticker aka an infinite loop.
2023-02-04 22:47:53 +03:00
Sergey Stepanov
652ffe37cc
Do not track DTLS errors (client disconnect) on WebRTC close 2023-02-04 20:32:16 +03:00
Sergey Stepanov
0d7467048a
Log abnormal WebRTC sample writes 2023-02-04 20:30:37 +03:00
Sergey Stepanov
c1c88cd2ad
Revert altRepo param for n64 (due to broken Windows cores for older GPUs in the Libretro) 2023-02-01 16:39:15 +03:00
Sergey Stepanov
46259e924d
Don't force alt repo for n64 cores 2023-02-01 16:33:01 +03:00
Sergey Stepanov
0c3b8ab8d7
Update dependencies 2023-01-31 22:55:11 +03:00
sergystepanov
2b81c3fb87
Some optimizations (#387)
- Fixed broken image cache for the first stage RGBA frames. It was not a thread-safe one, which led to image tearing (parts of old images in multiple consecutive frames).
- 180 flip function for the OpenGL coordinate system has been moved into the rotation part.
- Optimized YUV converter.
- Optimized color converters:
  - Use __restrict pointers.
  - Draw image pixels with the faster bitwise operators and pointer arithmetic as 32/16bit LE numbers (may break on ARM devices like RPi). 
  - Pass uints for less num conversions.
  - Much faster XRGB -> RGBA conversion with Go's stdlib 32bit flip.
- Wrapped RGBA images into a custom struct in order to bypass opacity tests for the standard Go png functions, which needed for the PNG file export. Before that we set RGBx opacity byte explicitly during the pixel format conversions (much slower).
- Made Libretro core shutdown more deterministic. When we run a C core separately from the main Go process we have to make sure that the C core is not doing anything in its syscall while we stopping the emulator. Basically, a blocking call may be suspended on the Go's side while the other goroutines have no knowledge of that.
- Less info level logs.
- Added recording user label.
- Enabled RTCP sender reports by default, which may help with A/V sync.
- Check onMessage webrtc handler if it's set. May crash the program if not.
- Fixed some make dirs permissions.
- Enabled console colors (since MS has finally fixed their bloody Terminal).
- Disable log in some tests.
- Updated deps.
2023-01-31 22:22:03 +03:00
Sergey Stepanov
c641065564
Enable RTCP sender reports by default 2023-01-15 01:07:53 +03:00
Sergey Stepanov
2df0135604
Merge remote-tracking branch 'origin/master' 2023-01-15 01:07:04 +03:00
Sergey Stepanov
cd90bfe58d
Enable RTCP sender reports by default 2023-01-15 01:06:50 +03:00
Sergey Stepanov
d12218e9db
Enable RTCP sender reports by default 2023-01-15 01:05:08 +03:00
Sergey Stepanov
07bc3d3a39
Remove broken image cache for now 2023-01-11 17:08:55 +03:00
sergystepanov
3bd959b4ef
Refactored v3 (#350)
This PR contains refactored code.

**Changelog**
- Added new net code (the communication architecture was left intact).
- All network client IDs now have custom type `network.Uid` backed by github.com/rs/xid lib.
  ```
  The string representation of a UUID takes 32 bytes, and the new type will take just 16.
  Because of Golang JSON serialization problems with omitting zero-length empty slices (it can't) 
  and the need to use UID values as map keys (maps don't support slices as keys), 
  IDs are stored as strings (for now).
  ```
- A whole new WebSocket client/server implementation was added, as well as a new communication layer with synchronous and async call handlers.
  - WebSocket connections now support dedicated Ping/Pong frames as opposed to original ping text messages.
  - Used Gorilla WebSocket library doesn't allow concurrent (simultaneous) reads and writes, so this part was handled via send channel synchronization.
- New API structures can be found in the `pkg/api` folder.
- New communication protocol can be found in the `pkg/com/*` folder.
- Updated communication protocol is based on JSON-encoded messaging through WebSocket and has the following structure:
  ```
  Packet
    [id] string — a globally unique identification tag for the packet to track it trough a chain of requests.
    t uint8 — contains packet type information (i.e. INIT_PACKET, SDP_OFFER_PACKET, ...).
    [p] any — contains packet data (any type).

  Each packet is a text message in JSON-serialized form (WebSocket control frames obviously not).
  ```
  ```
  The main principle of this protocol and the duplex data exchange is:
  the one who initializes connection is called a client, and 
  the one who is being connected to is called a server. 
  With the current architecture, the coordinator is the server, the user browsers and workers are the clients.

            ____           ____
           ↓    ↑         ↑    ↓
     browser ⟶ coordinator ⟵ worker
       (c)          (s)         (c)

  One of the most crucial performance vise parts of these interactions is that 
  all the server-initiated calls to clients should be asynchronous!
  ```
  - In order to track synchronous calls (packets) with an asynchronous protocol, such as WebSocket, each packet may have an `id` that should be copied in all subsequent requests/responses.
  - The old `sessionID` param was replaced by `id` that should be stored inside the `p` (payload) part of the packet.
- It is possible to skip the default ping check for all connected workers on every user connection and just pick the first available with the new roundRobin param in the coordinator config file `coordinator.roundRobin: true/false`.
- Added a dedicated package for the system API (pkg/api/*).
- Added structured logging system (zerolog) for better logging and cloud services integration.
- Added a visual representation of the network message exchange in logs:
  ```
  ...
  01:00:01.1078 3f98 INF w → c Handshake ws://localhost:8000/wso
  01:00:01.1138  994 INF c ← w Handshake localhost:8000
  01:00:01.1148  994 INF c ← w Connect cid=cep.hrg
  01:00:01.1158  994 DBG c     connection id has been changed to cepl7obdrc3jv66kp2ug cid=cep.hrg
  01:00:01.1158 3f98 INF w → c Connect cid=cep.2ug
  01:00:01.1158  994 INF c     New worker / addr: localhost, ...
  01:00:01.1158 3f98 INF w     Connected to the coordinator localhost:8000 cid=cep.2ug
  01:00:02.5834  994 INF c ← u Handshake localhost:8000
  01:00:02.6175  994 INF c ← u Connect cid=cep.hs0
  01:00:02.6209  994 INF c     Search available workers cid=cep.hs0
  01:00:02.6214  994 INF c     Found next free worker cid=cep.hs0
  01:00:02.6220  994 INF c → u InitSession cid=cep.hs0
  01:00:02.6527  994 INF c ← u WebrtcInit cid=cep.hs0
  01:00:02.6527  994 INF c → w ᵇWebrtcInit cid=cep.hrg
  01:00:02.6537 3f98 INF w ← c WebrtcInit cid=cep.2ug
  01:00:02.6537 3f98 INF w     WebRTC start cid=cep.2ug
  ...
  ```
- Replaced a monstrous Prometheus metrics lib.
- Removed spflag dependency.
- Added new `version` config file param/constant for compatibility reasons.
- Bump the minimum required version for Go to 1.18 due to use of generics.
- Opus encoder now is cached and the default config is 96Kbps, complexity 5 (was 196Kbps, 8).
- Changed the default x264 quality parameters to `crf 23 / superfast / baseline` instead of `crf 17 / veryfast / main`.
- Added a separate WebRTC logging config param `webrtc.logLevel`.
- Worker now allocates much less memory.
- Optimized and fixed RGB to YUV converter.
- `--v=5` logging cmd flag was removed and replaced with the `debug` config parameter.


**Breaking changes (migration to v3)**
- Coordinator server API changes, see web/js/api/api.js.
- Coordinator client event API changes:
  - c `GAME_PLAYER_IDX_CHANGE` (string) -> `GAME_PLAYER_IDX` (number)
  - c `GAME_PLAYER_IDX` -> `GAME_PLAYER_IDX_SET`
  - c `MEDIA_STREAM_INITIALIZED` -> `WEBRTC_NEW_CONNECTION`
  - c `MEDIA_STREAM_SDP_AVAILABLE` -> `WEBRTC_SDP_OFFER`
  - c `MEDIA_STREAM_CANDIDATE_ADD` -> `WEBRTC_ICE_CANDIDATE_RECEIVED`
  - c `MEDIA_STREAM_CANDIDATE_FLUSH` -> `WEBRTC_ICE_CANDIDATES_FLUSH`
  - x `MEDIA_STREAM_READY` -> **removed**
  - c `CONNECTION_READY` -> `WEBRTC_CONNECTION_READY`
  - c `CONNECTION_CLOSED` -> `WEBRTC_CONNECTION_CLOSED`
  - c `GET_SERVER_LIST` -> `WORKER_LIST_FETCHED`
  - x `KEY_STATE_UPDATED` -> **removed**
  - n `WEBRTC_ICE_CANDIDATE_FOUND`
  - n `WEBRTC_SDP_ANSWER`
  - n `MESSAGE`
- `rtcp` module renamed to `webrtc`.
- Controller state equals Libretro controller state (changed order of bits), see: web/js/input/input.js.
- Added new `coordintaor.selector` config param that changes the selection algorithm for workers. By default it will select any free worker. Set this param to `ping` for the old behavior.
- Changed the name of the `webrtc.iceServers.url` config param to `webrtc.iceServers.urls`.
2023-01-09 23:20:22 +03:00
Sergey Stepanov
14b589477f
Disable save/load for libCo until it's fixed 2022-12-10 01:23:57 +03:00
Sergey Stepanov
e3b2175420
Update libretro.h 2022-12-01 22:54:04 +03:00
sergystepanov
6b6c391f81
Faster image processing (#382)
* Calculate frame duration in ns

* Reuse single OpenGL byte buffer

* Add threaded a/v processing

* Check min threads in opts

* Return missing audio sample
2022-11-24 13:44:00 +03:00
giongto35
7d355611eb
Update README.md 2022-11-13 12:38:26 -08:00
giongto35
be445d281a
Update README.md 2022-11-13 12:38:04 -08:00
sergystepanov
d79e772471
Use custom OpenGL bindings (#378) 2022-08-18 22:18:25 +03:00
sergystepanov
8e49b05226
Add ICE config from the CLOUD_GAME_WEBRTC_ICESERVERS_0_ env vars (#377) 2022-08-17 23:06:27 +03:00
sergystepanov
54ee9a7af3
Savestate compression (#374)
Added the optional saveCompression configuration parameter which enables ZIP compression of the emulator states, reducing files sizes by the factor of 10x (most of these state files store zeroes).
2022-08-05 22:44:40 +03:00
sergystepanov
6da953ffc6
Add ARM64 MacOs mapping for core-auto-downloader (#376)
The auto-downloader had missing mapping for the darwin:arm64 os:arch mapping. Mupen64 (N64) is still missing in the official RetroArch repo https://buildbot.libretro.com/ and requires a manual build.
2022-08-02 15:03:23 +03:00
Sergey Stepanov
e214025bef
Disable excessive autosave logging 2022-07-26 12:28:02 +03:00
Valniae
1750d48b89
Timely auto save (#373)
Add autosave for the emulator states. Controlled with the autosaveSec param.
2022-07-26 12:25:36 +03:00
Sergey Stepanov
8025cc1d87
Make client ICE logging a bit clearer 2022-07-13 00:00:54 +03:00
sergystepanov
e53cf45fa7
Main thread lock refactoring (#367)
Use slim main thread locking function for macOS instead of a lib.
2022-06-12 13:39:23 +03:00
Sergey Stepanov
af98bddb14
Remove emu mock leftovers 2022-06-10 01:22:13 +03:00
sergystepanov
b31d8a7029
Add new option for alt cores download (#368)
Add the new option for Libretro cores: `altRepo` true | false, which sets the second repo download config as primary.
That will add, for example, the option of using specific cores from a custom repo when something will be broken in the main repo.
2022-06-10 01:20:21 +03:00
giongto35
b7c437e5c0
Update README.md 2022-05-21 15:58:32 +08:00
Sergey Stepanov
be0e00c7bf
Use display: none in the hide function, so it won't take any space 2022-04-18 22:47:35 +03:00
Sergey Stepanov
ed60ccf41e
Fix visibility of the help overlay 2022-04-18 19:13:26 +03:00
sergystepanov
9b2e41ff86
[skip ci] Update the game example link 2022-04-17 23:36:10 +03:00
sergystepanov
89b1683438
[skip ci] Update the game example link 2022-04-17 23:34:46 +03:00
Sergey Stepanov
4baccc4701
Allow adjusting log levels on the fly 2022-04-11 17:46:10 +03:00
Sergey Stepanov
7a0bf5864e
Use CGO with coordinator 2022-04-11 09:31:14 +03:00
Sergey Stepanov
c8accaeb62
Update dependencies 2022-04-11 09:24:10 +03:00
Sergey Stepanov
0665c3e607
Remove heavy Google Cloud Storage lib 2022-04-11 09:22:06 +03:00
Sergey Stepanov
3bbf075f92
Log lines in the browser console 2022-04-11 09:16:08 +03:00
Sergey Stepanov
63d8a0354c
Remove unused CSS in the worker list element 2022-04-09 10:46:10 +03:00
Sergey Stepanov
046e272c5f
Clean old Docker images when deploying 2022-04-09 10:32:31 +03:00
Sergey Stepanov
fac6cc4495
Generate xIds on the worker's side 2022-04-09 10:27:04 +03:00
sergystepanov
59c3d4a6c7
Pull all the images when deploying 2022-04-09 09:43:51 +03:00
Sergey Stepanov
c190177955
Show active workers in the debug mode 2022-04-08 20:11:28 +03:00
Sergey Stepanov
76c66339aa
Allow only available workers 2022-04-08 18:39:24 +03:00
sergystepanov
9ad3c98a7d
Rework worker selection feature (#365)
We add a new option for manual worker or machine (server with multiple workers) select, depending on the new coordinator option `coordinator.debug` which by default allows machine selection.
2022-04-07 21:04:30 +03:00
Sergey Stepanov
12352d5677
Open GitHub links in a new tab 2022-03-22 22:08:36 +03:00
Sergey Stepanov
242b8f0a1f
Update Go version to 1.18 for builds 2022-03-21 12:15:14 +03:00
Sergey Stepanov
3292964f01
Fix pubsub listeners indexing overlap 2022-01-02 06:58:55 +03:00
Sergey Stepanov
2a283e24db
Remove DebugHost and Environment config params 2021-12-30 11:06:35 +03:00
Sergey Stepanov
02a061a580
Extract audio buffer and resampler 2021-12-25 21:50:33 +03:00
Sergey Stepanov
2f841f8946
Add manual config mangling for worker 2021-12-21 23:23:15 +03:00
Sergey Stepanov
f688f3c5f4
Add optional lite ICE agent config param 2021-12-20 22:28:54 +03:00
Sergey Stepanov
589cda91f4
Add optional DTLS role config param 2021-12-20 22:18:43 +03:00
Sergey Stepanov
24ff0f2ea2
Close WebRTC connections on disconnect
Not closing RTCPeerConnection and co will cause eventually high processor load in Chrome.
Also, Chrome has some GC issues related to WebRTC, see: https://bugs.chromium.org/p/chromium/issues/detail?id=825576.
2021-12-17 16:10:51 +03:00
Sergey Stepanov
ad822a624d
Add optional Origin handling for Websockets 2021-12-15 17:58:49 +03:00
Sergey Stepanov
535177bd46
Fix recording handling when it's disabled 2021-12-05 22:11:29 +03:00
sergystepanov
1271aa8438
Game recording support (#356)
This feature adds the ability to record game sessions as raw a/v media files.
2021-12-04 14:20:38 +03:00
Sergey Stepanov
17bec2e987
Remove unused ReTime Pion interceptor 2021-11-22 20:03:35 +03:00
Sergey Stepanov
339750a978
Calculate emulation frames duration directly 2021-11-22 20:01:46 +03:00
sergystepanov
9acbecb813
WebRTC single port support (#354)
Add the new `webrtc.singlePort` config option which forces the WebRTC server to listen on this port only.
2021-11-02 17:05:31 +03:00
Sergey Stepanov
db52ca3f2f
Remove unused IDE/Git files 2021-10-17 19:52:43 +03:00
Sergey Stepanov
dfe5622920
Remove Docker .env 2021-10-15 11:53:19 +03:00
Sergey Stepanov
a73c0e8c5f
Update re-time interceptor.
Update re-time video frame interceptor to the latest pion WebRTC changes and keep it shared between all RTC peer connections.
2021-10-14 19:47:08 +03:00
Masaya Watanabe
c27e1fe2ab
Don't ignore cloud save download errors (#352) 2021-10-08 19:47:30 +03:00
Sergey Stepanov
be52ee18c5
Use default WebRTC codecs 2021-09-28 13:28:44 +03:00
Sergey Stepanov
891e397104
Update Buildbot arm file ext 2021-09-27 21:49:52 +03:00
Sergey Stepanov
03107ba902
Use unsigned return value for the Libretro frame rotation 2021-09-23 14:42:16 +03:00
sergystepanov
69ff8ae896
Update cloud data storage functionality (#349)
Add Oracle Data Storage support for cloud saves.
2021-09-20 10:17:59 +03:00
340 changed files with 28240 additions and 17848 deletions

View file

@ -1,18 +1,9 @@
.git/
.github/
.idea/
.vscode/
.gitignore
.editorconfig
.env
docker-compose.yml
Dockerfile
LICENSE
README.md
bin/
docs/
release/
assets/games/
/**
!cmd/
!pkg/
!scripts/
!web/
!go.mod
!go.sum
!LICENSE
!Makefile

View file

@ -1,4 +0,0 @@
root = true
[*.md]
trim_trailing_whitespace = false

1
.env
View file

@ -1 +0,0 @@
CLOUD_GAME_GAMES_PATH=./assets/games

16
.gitattributes vendored
View file

@ -1,16 +0,0 @@
* linguist-vendored
*.go linguist-vendored=false
* text=auto eol=lf
# Explicitly declare text files you want to always be normalized and converted
# to native line endings on checkout.
*.c text
*.h text
# Declare files that will always have CRLF line endings on checkout.
*.sln text eol=crlf
# Denote all files that are truly binary and should not be modified.
*.png binary
*.jpg binary

View file

@ -1,5 +1,5 @@
# ------------------------------------------------------------
# Build workflow (Linux x64, macOS x64, Windows x64)
# Build and test workflow (Linux x64, macOS x64, Windows x64)
# ------------------------------------------------------------
name: build
@ -16,101 +16,73 @@ on:
jobs:
build:
name: Build
strategy:
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
step: [ build, check ]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: actions/setup-go@v2
- uses: actions/setup-go@v5
with:
go-version: ^1.17
go-version: 'stable'
- name: Get Linux dev libraries and tools
- name: Linux
if: matrix.os == 'ubuntu-latest'
env:
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
run: |
sudo apt-get -qq update
sudo apt-get -qq install -y make pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev libgl1-mesa-glx
sudo apt-get -qq install -y \
make pkg-config \
libvpx-dev libx264-dev libopus-dev libyuv-dev libjpeg-turbo8-dev \
libsdl2-dev libgl1 libglx-mesa0 libspeexdsp-dev
make build
xvfb-run --auto-servernum make test verify-cores
- name: Get MacOS dev libraries and tools
if: matrix.os == 'macos-latest'
- name: macOS
if: matrix.os == 'macos-12'
run: |
brew install pkg-config libvpx x264 opus sdl2
brew install libvpx x264 sdl2 speexdsp
make build test verify-cores
- name: Get Windows dev libraries and tools
- uses: msys2/setup-msys2@v2
if: matrix.os == 'windows-latest'
uses: msys2/setup-msys2@v2
with:
msystem: MINGW64
msystem: ucrt64
path-type: inherit
release: false
install: >
mingw-w64-x86_64-gcc
mingw-w64-x86_64-pkgconf
mingw-w64-x86_64-dlfcn
mingw-w64-x86_64-libvpx
mingw-w64-x86_64-opus
mingw-w64-x86_64-x264-git
mingw-w64-x86_64-SDL2
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-pkgconf
mingw-w64-ucrt-x86_64-dlfcn
mingw-w64-ucrt-x86_64-libvpx
mingw-w64-ucrt-x86_64-opus
mingw-w64-ucrt-x86_64-libx264
mingw-w64-ucrt-x86_64-SDL2
mingw-w64-ucrt-x86_64-libyuv
mingw-w64-ucrt-x86_64-libjpeg-turbo
mingw-w64-ucrt-x86_64-speexdsp
- name: Get Windows OpenGL drivers
if: matrix.step == 'check' && matrix.os == 'windows-latest'
- name: Windows
if: matrix.os == 'windows-latest'
env:
MESA_VERSION: '24.0.7'
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
shell: msys2 {0}
run: |
wget -q https://github.com/pal1000/mesa-dist-win/releases/download/20.2.1/mesa3d-20.2.1-release-mingw.7z
"/c/Program Files/7-Zip/7z.exe" x mesa3d-20.2.1-release-mingw.7z -omesa
echo -e " 2\r\n 8\r\n " >> commands
set MSYSTEM=UCRT64
wget -q https://github.com/pal1000/mesa-dist-win/releases/download/$MESA_VERSION/mesa3d-$MESA_VERSION-release-msvc.7z
"/c/Program Files/7-Zip/7z.exe" x mesa3d-$MESA_VERSION-release-msvc.7z -omesa
echo -e " 1\r\n 9\r\n " >> commands
./mesa/systemwidedeploy.cmd < ./commands
make build test verify-cores
- name: Build Windows app
if: matrix.step == 'build' && matrix.os == 'windows-latest'
shell: msys2 {0}
run: |
make build
- name: Build Linux app
if: matrix.step == 'build' && matrix.os == 'ubuntu-latest'
run: |
make build
- name: Build macOS app
if: matrix.step == 'build' && matrix.os == 'macos-latest'
run: |
make build
- name: Verify core rendering (windows-latest)
if: matrix.step == 'check' && matrix.os == 'windows-latest' && always()
shell: msys2 {0}
env:
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
run: |
GL_CTX=-autoGlContext make verify-cores
- name: Verify core rendering (ubuntu-latest)
if: matrix.step == 'check' && matrix.os == 'ubuntu-latest' && always()
env:
MESA_GL_VERSION_OVERRIDE: 3.3COMPAT
run: |
GL_CTX=-autoGlContext xvfb-run --auto-servernum make verify-cores
- name: Verify core rendering (macos-latest)
if: matrix.step == 'check' && matrix.os == 'macos-latest' && always()
run: |
make verify-cores
- uses: actions/upload-artifact@v2
if: matrix.step == 'check' && always()
- uses: actions/upload-artifact@v4
if: always()
with:
name: emulator-test-frames
name: emulator-test-frames-${{ matrix.os }}
path: _rendered/*.png
build_docker:
name: Build (docker)
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v2
- run: docker build --build-arg VERSION=$(./scripts/version.sh) .

View file

@ -0,0 +1,40 @@
version: 4
coordinator:
debug: true
server:
address:
frameOptions: SAMEORIGIN
https: true
tls:
domain: cloudretro.io
analytics:
inject: true
gtag: UA-145078282-1
worker:
debug: true
network:
coordinatorAddress: cloudretro.io
publicAddress: cloudretro.io
secure: true
server:
https: true
tls:
address: :444
# domain: cloudretro.io
emulator:
libretro:
logLevel: 1
cores:
list:
dos:
uniqueSaveDir: true
mame:
options:
"fbneo-diagnostic-input": "Hold Start"
nes:
scale: 2
snes:
scale: 2

View file

@ -1,6 +0,0 @@
CLOUD_GAME_COORDINATOR_ANALYTICS_GTAG=UA-145078282-1
CLOUD_GAME_COORDINATOR_ANALYTICS_INJECT=true
CLOUD_GAME_COORDINATOR_SERVER_ADDRESS=
CLOUD_GAME_COORDINATOR_SERVER_HTTPS=true
CLOUD_GAME_COORDINATOR_SERVER_TLS_DOMAIN=cloudretro.io
CLOUD_GAME_ENVIRONMENT=prod

View file

@ -1,6 +1,6 @@
COORDINATORS="167.172.70.98 cloudretro.io"
DOCKER_IMAGE_TAG=dev
DO_ADDRESS_LIST="cloud-gaming cloud-gaming-eu cloud-gaming-usw"
SPLIT_HOSTS=1
COORDINATORS="138.68.48.200"
DOCKER_IMAGE_TAG=master
#DO_ADDRESS_LIST="cloud-gaming cloud-gaming-eu cloud-gaming-usw"
#SPLIT_HOSTS=1
USER=root
WORKERS=${WORKERS:-5}
WORKERS=${WORKERS:-4}

View file

@ -1,7 +0,0 @@
CLOUD_GAME_WORKER_NETWORK_COORDINATORADDRESS=cloudretro.io
CLOUD_GAME_WORKER_NETWORK_PUBLICADDRESS=cloudretro.io
CLOUD_GAME_WORKER_NETWORK_SECURE=true
CLOUD_GAME_WORKER_SERVER_ADDRESS=:80
CLOUD_GAME_WORKER_SERVER_HTTPS=true
CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:443
CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io

View file

@ -54,6 +54,10 @@ IP_LIST=${IP_LIST:-}
# a list of machines mark some addresses to deploy only a coordinator there
COORDINATORS=${COORDINATORS:-}
if [ -z "$SPLIT_HOSTS" ]; then
IP_LIST+=$COORDINATORS
fi
# Digital Ocean operations
#DO_TOKEN
DO_ADDRESS_LIST=${DO_ADDRESS_LIST:-}
@ -64,7 +68,7 @@ REMOTE_WORK_DIR=${REMOTE_WORK_DIR:-"/cloud-game"}
DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG:-latest}
echo "Docker tag:$DOCKER_IMAGE_TAG"
# the total number of worker replicas to deploy
WORKERS=${WORKERS:-5}
WORKERS=${WORKERS:-4}
USER=${USER:-root}
compose_src=$(cat $LOCAL_WORK_DIR/docker-compose.yml)
@ -124,7 +128,7 @@ echo "IPs:" $IP_LIST
# Run command builder
#
# By default it will run docker-compose with both coordinator and worker apps.
# By default it will run docker compose with both coordinator and worker apps.
# With the SPLIT_HOSTS parameter specified, it will run either coordinator app
# if the current server address is found in the IP_LIST variable, otherwise it
# will run just the worker app.
@ -142,28 +146,39 @@ for ip in $IP_LIST; do
fi
# build run command
cmd="ZONE=\$zone docker-compose up -d --remove-orphans --scale worker=\${workers:-$WORKERS}"
cmd="ZONE=\$zone docker compose up -d --remove-orphans"
if [ ! -z "$SPLIT_HOSTS" ]; then
cmd+=" worker"
deploy_coordinator=0
deploy_worker=1
else
cmd+=" worker"
fi
# override run command
if [ ! -z "$SPLIT_HOSTS" ]; then
for addr in $COORDINATORS; do
if [ "$ip" == $addr ]; then
cmd="docker-compose up -d --remove-orphans coordinator"
cmd="docker compose up -d --remove-orphans coordinator"
deploy_coordinator=1
deploy_worker=0
break
fi
done
else
cmd+=" coordinator"
fi
# build Docker container env file
run_env=""
custom_config=""
if [[ ! -z "${ENV_DIR}" ]]; then
env_f=$ENV_DIR/config.yaml
if [[ -e "$env_f" ]]; then
echo "config.yaml found"
custom_config=$(cat $env_f)
fi
if [ $deploy_coordinator == 1 ]; then
env_f=$ENV_DIR/coordinator.env
if [[ -e "$env_f" ]]; then
@ -190,13 +205,13 @@ for ip in $IP_LIST; do
run="#!/bin/bash"$'\n'
run+=$(remote_run_commands "$ENV_DIR")$'\n'
run+=$(remote_run_commands "$PROVIDER_DIR")$'\n'
run+="IMAGE_TAG=$DOCKER_IMAGE_TAG APP_DIR=$REMOTE_WORK_DIR $cmd"
run+="IMAGE_TAG=$DOCKER_IMAGE_TAG APP_DIR=$REMOTE_WORK_DIR WORKER_REPLICAS=$WORKERS $cmd"
echo ""
echo "run.sh:"$'\n'"$run"
echo ""
# !to add docker-compose install / warning
# !to add docker compose install / warning
# custom scripts
remote_sudo_run_once $ip "$PROVIDER_DIR" "$ssh_i"
@ -205,13 +220,13 @@ for ip in $IP_LIST; do
echo "Update the remote host"
ssh -o ConnectTimeout=10 $USER@$ip ${ssh_i:-} "\
docker-compose -v; \
docker compose version; \
mkdir -p $REMOTE_WORK_DIR; \
cd $REMOTE_WORK_DIR; \
mkdir -p $REMOTE_WORK_DIR/home; \
echo \"$custom_config\" > $REMOTE_WORK_DIR/home/config.yaml; \
echo '$compose_src' > ./docker-compose.yml; \
echo '$run_env' > ./run.env; \
IMAGE_TAG=$DOCKER_IMAGE_TAG docker-compose pull coordinator; \
echo '$run' > ./run.sh; \
chmod +x ./run.sh; \
./run.sh"
docker compose down; \
IMAGE_TAG=$DOCKER_IMAGE_TAG docker compose pull; \
docker compose up -d;"
done

View file

@ -1,36 +1,93 @@
version: "3.4"
x-params:
&default-params
env_file: run.env
image: ghcr.io/giongto35/cloud-game/cloud-game:${IMAGE_TAG:-latest}
x-params: &default-params
image: ghcr.io/giongto35/cloud-game/cloud-game:${IMAGE_TAG:-master}
network_mode: "host"
privileged: true
restart: always
security_opt:
- seccomp=unconfined
logging:
driver: "json-file"
options:
max-size: "64m"
max-file: "5"
compress: "true"
driver: "journald"
x-worker: &worker
depends_on:
- coordinator
command: ./worker
volumes:
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
- ${APP_DIR:-/cloud-game}/libretro:/usr/local/share/cloud-game/libretro
- ${APP_DIR:-/cloud-game}/home:/root/.cr
- x11:/tmp/.X11-unix
healthcheck:
test: curl -f https://cloudretro.io/echo || exit 1
interval: 1m
timeout: 10s
retries: 3
start_period: 40s
start_interval: 5s
services:
coordinator:
<<: *default-params
command: coordinator --v=5
volumes:
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
worker:
<<: *default-params
command: ./coordinator
environment:
- MESA_GL_VERSION_OVERRIDE=3.3
entrypoint: [ "/bin/sh", "-c", "xvfb-run -a $$@", "" ]
command: worker --v=5 --zone=${ZONE:-}
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
volumes:
- ${APP_DIR:-/cloud-game}/cache:/usr/local/share/cloud-game/assets/cache
- ${APP_DIR:-/cloud-game}/cores:/usr/local/share/cloud-game/assets/cores
- ${APP_DIR:-/cloud-game}/games:/usr/local/share/cloud-game/assets/games
- ${APP_DIR:-/cloud-game}/home:/root/.cr
worker01:
<<: [ *default-params, *worker ]
environment:
- DISPLAY=:99
- MESA_GL_VERSION_OVERRIDE=4.5
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:444
healthcheck:
test: curl -f https://cloudretro.io:444/echo || exit 1
worker02:
<<: [ *default-params, *worker ]
environment:
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:445
- DISPLAY=:99
- MESA_GL_VERSION_OVERRIDE=4.5
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
healthcheck:
test: curl -f https://cloudretro.io:445/echo || exit 1
worker03:
<<: [ *default-params, *worker ]
environment:
- DISPLAY=:99
- MESA_GL_VERSION_OVERRIDE=4.5
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:446
healthcheck:
test: curl -f https://cloudretro.io:446/echo || exit 1
worker04:
<<: [ *default-params, *worker ]
environment:
- DISPLAY=:99
- MESA_GL_VERSION_OVERRIDE=4.5
- CLOUD_GAME_LIBRARY_BASEPATH=/usr/local/share/cloud-game/assets/games
- CLOUD_GAME_EMULATOR_LIBRETRO_CORES_PATHS_LIBS=/usr/local/share/cloud-game/assets/cores
- CLOUD_GAME_WORKER_SERVER_TLS_DOMAIN=cloudretro.io
- CLOUD_GAME_WORKER_SERVER_TLS_ADDRESS=:447
healthcheck:
test: curl -f https://cloudretro.io:447/echo || exit 1
xvfb:
image: kcollins/xvfb:latest
volumes:
- x11:/tmp/.X11-unix
command: [ ":99", "-screen", "0", "320x240x16" ]
volumes:
x11:

View file

@ -16,7 +16,7 @@ jobs:
key: ${{ secrets.SSH_PRIVATE_KEY }}
known_hosts: 'PLACEHOLDER'
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Deploy to all servers
env:

12
.github/workflows/docker_build.yml vendored Normal file
View file

@ -0,0 +1,12 @@
name: docker_build
on:
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: DOCKER_BUILDKIT=1 docker build --build-arg VERSION=$(./scripts/version.sh) .

48
.github/workflows/docker_publish.yml vendored Normal file
View file

@ -0,0 +1,48 @@
# ----------------------------------------------------------------------------------
# Publish Docker image from the current master branch or v* into Github repository
# ----------------------------------------------------------------------------------
name: docker-publish
on:
push:
branches:
- master
tags:
- 'v*'
jobs:
docker-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
- name: Docker meta
id: meta
uses: docker/metadata-action@v4
with:
images: ghcr.io/${{ github.repository }}/cloud-game
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- uses: docker/setup-buildx-action@v2
- uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v4
with:
build-args: VERSION=${{ env.V }}
context: .
push: true
provenance: false
sbom: false
tags: |
${{ steps.meta.outputs.tags }}
labels: |
${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,31 +0,0 @@
# ------------------------------------------------------------------------
# Publish Docker image from the stable snapshot into Github repository
# ------------------------------------------------------------------------
name: publish-stable
on:
push:
tags:
- 'v*'
jobs:
docker-publish-stable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
- uses: docker/build-push-action@v1
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
add_git_labels: true
tags: latest,${{ env.TAG }}
build_args: VERSION=${{ env.V }}
registry: docker.pkg.github.com
repository: ${{ github.REPOSITORY }}/cloud-game

View file

@ -1,29 +0,0 @@
# ----------------------------------------------------------------------------
# Publish Docker image from the current master branch into Github repository
# ----------------------------------------------------------------------------
name: publish-unstable
on:
push:
branches:
- master
jobs:
docker-publish-unstable:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: echo "V=$(./scripts/version.sh)" >> $GITHUB_ENV
- uses: docker/build-push-action@v1
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
add_git_labels: true
tags: dev
build_args: VERSION=${{ env.V }}
registry: docker.pkg.github.com
repository: ${{ github.REPOSITORY }}/cloud-game

View file

@ -37,11 +37,11 @@ jobs:
env:
release-dir: _release
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- uses: actions/setup-go@v2
- uses: actions/setup-go@v4
with:
go-version: ^1.17
go-version: ^1.20
- name: Get Linux dev libraries and tools
if: matrix.os == 'ubuntu-latest'

15
.gitignore vendored
View file

@ -45,6 +45,11 @@ Network Trash Folder
Temporary Items
.apdisk
### SSL
*.crt
*.csr
*.key
### Production
DockerfileProd
key.json
@ -62,5 +67,15 @@ _output/
./build
release/
vendor/
tests/
!tests/e2e/
*.exe
.dockerignore
### Libretro
fbneo/
hi/
nvram/
*.mcd

79
DESIGNv2.md Normal file
View file

@ -0,0 +1,79 @@
# Cloud Gaming Service Design Document
Cloud Gaming Service contains multiple workers for gaming streams and a coordinator for distributing traffic and pairing
up connections.
## Coordinator
Coordinator is a web-frontend, load balancer and signalling server for WebRTC.
```
WORKERS
┌──────────────────────────────────┐
│ │
│ REGION 1 REGION 2 REGION N │
│ (US) (DE) (XX) │
│ ┌──────┐ ┌──────┐ ┌──────┐ |
COORDINATOR │ │WORKER│ │WORKER│ │WORKER│ |
┌───────────┐ │ └──────┘ └──────┘ └──────┘ |
│ │ ───────────────────────HEALTH────────────────────► │ • • • |
│ HTTP/WS │ ◄─────────────────────REG/DEREG─────────────────── │ • • • |
│┌─────────┐│ │ • • • |
│| |│ USER │ ┌──────┐* ┌──────┐ ┌──────┐ |
│└─────────┘│ ┌──────┐ │ │WORKER│ │WORKER│ │WORKER│ |
│ │ ◄──(1)CONNECT───────── │ │ ────(3)SELECT────► │ └──────┘ └──────┘ └──────┘ |
│ │ ───(2)LIST WORKERS───► │ │ ◄───(4)STREAM───── │ │
└───────────┘ └──────┘ │ * MULTIPLAYER │
│ ┌──────┐────► ONE GAME │
│ ┌───►│WORKER│◄──┐ │
│ │ └──────┘ │ │
│ │ ▲ ▲ │ │
│ ┌┴─┐ │ │ ┌┴─┐ |
│ │U1│ ┌─┴┐ ┌┴─┐ │U4│ |
│ └──┘ │U2│ │U3│ └──┘ |
│ └──┘ └──┘ |
│ |
└──────────────────────────────────┘
```
- (1) A user opens the main page of the app in the browser, i.e. connects to the coordinator.
- (2) The coordinator searches and serves a list of most suitable workers to the user.
- (3) The user proceeds with latency check of each worker from the list, then coordinator collects user-to-worker
latency data and picks the best candidate.
- (4) The coordinator sets up peer-to-peer connection between a worker and the user based on the WebRTC protocol and a
game hosted on the worker is streamed to the user.
## Worker
Worker is responsible for running and streaming games to users.
```
WORKER
┌─────────────────────────────────────────────────────────────────┐
│ EMULATOR WEBRTC │ BROWSER
│ ┌─────────────────┐ ENCODER ┌────────┐ │ ┌──────────┐
│ │ │ ┌─────────┐ | DMUX | | ───RTP──► | WEBRTC |
│ │ AUDIO SAMPLES │ ──PCM──► │ │ ──OPUS──► │ ┌──► │ │ ◄──SCTP── | |
│ │ VIDEO FRAMES │ ──RGB──► │ │ ──H264──► │ └──► | | └──────────┘ COORDINATOR
│ │ │ └─────────┘ │ │ │ • ┌─────────────┐
│ │ │ | MUX | | ───TCP──────── • ───────► | WEBSOCKET |
│ │ │ │ ┌── │ │ • └─────────────┘
| | | BINARY | ▼ | | BROWSER
│ │ INPUT STATE │ ◄───────────────────────────── │ • │ │ ┌──────────┐
│ │ │ │ ▲ │ │ ───RTP──► | WEBRTC |
│ └─────────────────┘ HTTP/WS | └── | │ ◄──SCTP── │ │
│ ┌─────────┐ └────────┘ │ └──────────┘
| | | |
| └─────────┘ |
└─────────────────────────────────────────────────────────────────┘
```
- After coordinator matches the most appropriate server (peer 1) to the user (peer 2), a WebRTC peer-to-peer handshake
will be conducted. The coordinator will help initiate the session between the two peers over a WebSocket connection.
- The worker either spawns new rooms running game emulators or connects users to existing rooms.
- Raw image and audio streams from the emulator are captured and encoded to a WebRTC-supported streaming format. Next,
these stream are piped out (dmux) to all users in the room.
- On the other hand, input from players is sent to the worker over WebRTC DataChannel. The game logic on the emulator
will be updated based on the input stream of all players, for that each stream is multiplexed (mux) into one.
- Game states (saves) are stored in cloud storage, so all distributed workers can keep game states in sync and players
can continue their games where they left off.

View file

@ -1,61 +1,101 @@
# The base cloud-game image
ARG BUILD_PATH=/go/src/github.com/giongto35/cloud-game
ARG BUILD_PATH=/tmp/cloud-game
ARG VERSION=master
# build image
FROM debian:bullseye-slim AS build
# base build stage
FROM ubuntu:plucky AS build0
ARG GO=1.26rc1
ARG GO_DIST=go${GO}.linux-amd64.tar.gz
ADD https://go.dev/dl/$GO_DIST ./
RUN tar -C /usr/local -xzf $GO_DIST && \
rm $GO_DIST
ENV PATH="${PATH}:/usr/local/go/bin"
RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
ca-certificates \
make \
upx \
&& rm -rf /var/lib/apt/lists/*
# next conditional build stage
FROM build0 AS build_coordinator
ARG BUILD_PATH
ARG VERSION
ENV GIT_VERSION=${VERSION}
WORKDIR ${BUILD_PATH}
# system libs layer
RUN apt-get -qq update && apt-get -qq install --no-install-recommends -y \
gcc \
ca-certificates \
# by default we ignore all except some folders and files, see .dockerignore
COPY . ./
RUN --mount=type=cache,target=/root/.cache/go-build make build.coordinator
RUN find ./bin/* | xargs upx --best --lzma
WORKDIR /usr/local/share/cloud-game
RUN mv ${BUILD_PATH}/bin/* ./ && \
mv ${BUILD_PATH}/web ./web && \
mv ${BUILD_PATH}/LICENSE ./
RUN ${BUILD_PATH}/scripts/version.sh ./web/index.html ${VERSION} && \
${BUILD_PATH}/scripts/mkdirs.sh
# next worker build stage
FROM build0 AS build_worker
ARG BUILD_PATH
ARG VERSION
ENV GIT_VERSION=${VERSION}
WORKDIR ${BUILD_PATH}
# install deps
RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
build-essential \
libopus-dev \
libsdl2-dev \
libvpx-dev \
libyuv-dev \
libjpeg-turbo8-dev \
libx264-dev \
make \
libspeexdsp-dev \
pkg-config \
wget \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/*
# go setup layer
ARG GO=go1.17.linux-amd64.tar.gz
RUN wget -q https://golang.org/dl/$GO \
&& rm -rf /usr/local/go \
&& tar -C /usr/local -xzf $GO \
&& rm $GO
ENV PATH="${PATH}:/usr/local/go/bin"
# by default we ignore all except some folders and files, see .dockerignore
COPY . ./
RUN --mount=type=cache,target=/root/.cache/go-build make GO_TAGS=static,st build.worker
RUN find ./bin/* | xargs upx --best --lzma
# go deps layer
COPY go.mod go.sum ./
RUN go mod download
WORKDIR /usr/local/share/cloud-game
RUN mv ${BUILD_PATH}/bin/* ./ && \
mv ${BUILD_PATH}/LICENSE ./
RUN ${BUILD_PATH}/scripts/mkdirs.sh worker
# app build layer
COPY pkg ./pkg
COPY cmd ./cmd
COPY Makefile .
COPY scripts/version.sh scripts/version.sh
ARG VERSION
RUN GIT_VERSION=${VERSION} make build
FROM scratch AS coordinator
COPY --from=build_coordinator /usr/local/share/cloud-game /cloud-game
# autocertbot (SSL) requires these on the first run
COPY --from=build_coordinator /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
FROM ubuntu:plucky AS worker
RUN apt-get -q update && apt-get -q install --no-install-recommends -y \
curl \
libx11-6 \
libxext6 \
&& apt-get autoremove \
&& rm -rf /var/lib/apt/lists/* /var/log/* /usr/share/bug /usr/share/doc /usr/share/doc-base \
/usr/share/X11/locale/*
COPY --from=build_worker /usr/local/share/cloud-game /cloud-game
COPY --from=build_worker /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ADD https://github.com/sergystepanov/mesa-llvmpipe/releases/download/v1.0.0/libGL.so.1.5.0 \
/usr/lib/x86_64-linux-gnu/
RUN cd /usr/lib/x86_64-linux-gnu && \
ln -s libGL.so.1.5.0 libGL.so.1 && \
ln -s libGL.so.1 libGL.so
FROM worker AS cloud-game
# base image
FROM debian:bullseye-slim
ARG BUILD_PATH
WORKDIR /usr/local/share/cloud-game
COPY scripts/install.sh install.sh
RUN bash install.sh && \
rm -rf /var/lib/apt/lists/* install.sh
COPY --from=build ${BUILD_PATH}/bin/ ./
RUN cp -s $(pwd)/* /usr/local/bin
COPY assets/cores ./assets/cores
COPY configs ./configs
COPY web ./web
ARG VERSION
COPY scripts/version.sh version.sh
RUN bash ./version.sh ./web/index.html ${VERSION} && \
rm -rf version.sh
EXPOSE 8000 9000
COPY --from=coordinator /cloud-game ./
COPY --from=worker /cloud-game ./

View file

@ -1,11 +1,13 @@
# Makefile includes some useful commands to build or format incentives
# More commands could be added
# Variables
PROJECT = cloud-game
REPO_ROOT = github.com/giongto35
ROOT = ${REPO_ROOT}/${PROJECT}
CGO_CFLAGS='-g -O3'
CGO_LDFLAGS='-g -O3'
GO_TAGS=
.PHONY: clean test
fmt:
@goimports -w cmd pkg tests
@gofmt -s -w cmd pkg tests
@ -13,74 +15,54 @@ fmt:
compile: fmt
@go install ./cmd/...
check: fmt
@golangci-lint run cmd/... pkg/...
# @staticcheck -checks="all,-S1*" ./cmd/... ./pkg/... ./tests/...
dep:
go mod download
# go mod tidy
# NOTE: there is problem with go mod vendor when it delete github.com/gen2brain/x264-go/x264c causing unable to build. https://github.com/golang/go/issues/26366
#build.cross: build
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-darwin ./cmd/coordinator
# CGO_ENABLED=1 GOOS=darwin GOARC=amd64 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-darwin ./cmd/worker
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/coordinator-linu ./cmd/coordinator
# CC=arm-linux-musleabihf-gcc GOOS=linux GOARC=amd64 CGO_ENABLED=1 go build --ldflags '-linkmode external -extldflags "-static"' -o bin/worker-linux ./cmd/worker
# A user can invoke tests in different ways:
# - make test runs all tests;
# - make test TEST_TIMEOUT=10 runs all tests with a timeout of 10 seconds;
# - make test TEST_PKG=./model/... only runs tests for the model package;
# - make test TEST_ARGS="-v -short" runs tests with the specified arguments;
# - make test-race runs tests with race detector enabled.
TEST_TIMEOUT = 60
TEST_PKGS ?= ./cmd/... ./pkg/...
TEST_TARGETS := test-short test-verbose test-race test-cover
.PHONY: $(TEST_TARGETS) test tests
test-short: TEST_ARGS=-short
test-verbose: TEST_ARGS=-v
test-race: TEST_ARGS=-race
test-cover: TEST_ARGS=-cover
$(TEST_TARGETS): test
test: compile
@go test -timeout $(TEST_TIMEOUT)s $(TEST_ARGS) $(TEST_PKGS)
test-e2e: compile
@go test ./tests/e2e/...
cover:
@go test -v -covermode=count -coverprofile=coverage.out $(TEST_PKGS)
# @$(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $(COVERALLS_TOKEN)
clean:
@rm -rf bin
@rm -rf build
@go clean ./cmd/*
build:
build.coordinator:
mkdir -p bin/
CGO_ENABLED=0 go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
go build -buildmode=exe -tags static -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) -o bin/ ./cmd/worker
go build -ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" -o bin/ ./cmd/coordinator
build.worker:
mkdir -p bin/
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
go build -pgo=auto -buildmode=exe $(if $(GO_TAGS),-tags $(GO_TAGS),) \
-ldflags "-w -s -X 'main.Version=$(GIT_VERSION)'" $(EXT_WFLAGS) \
-o bin/ ./cmd/worker
build: build.coordinator build.worker
test:
go test -v ./pkg/...
verify-cores:
go test -run TestAllEmulatorRooms ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "../../../_rendered"
go test -run TestAll ./pkg/worker/room -v -renderFrames $(GL_CTX) -outputPath "./_rendered"
dev.build: compile build
dev.build-local:
mkdir -p bin/
CGO_ENABLED=0 go build -o bin/ ./cmd/coordinator
go build -buildmode=exe -o bin/ ./cmd/worker
go build -o bin/ ./cmd/coordinator
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} go build -pgo=auto -o bin/ ./cmd/worker
dev.run: dev.build-local
./bin/coordinator --v=5 &
./bin/worker --v=5
ifeq ($(OS),Windows_NT)
./bin/coordinator.exe & ./bin/worker.exe
else
./bin/coordinator & ./bin/worker
endif
dev.run.debug:
go build -race -o bin/ ./cmd/coordinator
CGO_CFLAGS=${CGO_CFLAGS} CGO_LDFLAGS=${CGO_LDFLAGS} \
go build -race -gcflags=all=-d=checkptr -o bin/ ./cmd/worker
./bin/coordinator & ./bin/worker
dev.run-docker:
docker rm cloud-game-local -f || true
CLOUD_GAME_GAMES_PATH=$(PWD)/assets/games docker-compose up --build
docker compose up --build
# RELEASE
# Builds the app for new release.
@ -97,8 +79,8 @@ dev.run-docker:
# Config params:
# - RELEASE_DIR: the name of the output folder (default: release).
# - CONFIG_DIR: search dir for core config files.
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; defalut: ldd).
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., mylib.so; default: .*so).
# - DLIB_TOOL: the name of a dynamic lib copy tool (with params) (e.g., ldd -x -y; default: ldd).
# - DLIB_SEARCH_PATTERN: a grep filter of the output of the DLIB_TOOL (e.g., my_lib.so; default: .*so).
# Be aware that this search pattern will return only matched regular expression part and not the whole line.
# de. -> abc def ghj -> def
# Makefile special symbols should be escaped with \.

View file

@ -11,37 +11,27 @@ on generic solution for cloudgaming
Discord: [Join Us](https://discord.gg/sXRQZa2zeP)
## Announcement
![screenshot](https://user-images.githubusercontent.com/846874/235532552-8c8253df-aa8d-48c9-a58e-3f54e284f86e.jpg)
**(Currently, I'm working on [CloudMorph](https://github.com/giongto35/cloud-morph): It offers more generic solution to
run any offline games/application on browser in Cloud Gaming
approach: [https://github.com/giongto35/cloud-morph](https://github.com/giongto35/cloud-morph))**
## Try it at **[cloudretro.io](https://cloudretro.io)**
Direct play an existing game: **[Pokemon Emerald](https://cloudretro.io/?id=1bd37d4b5dfda87c___Pokemon%20-%20Emerald%20Version%20(U))**
## Introduction
CloudRetro provides an open-source cloud gaming platform for retro games. It started as an experiment for testing cloud
gaming performance with [WebRTC](https://github.com/pion/webrtc/) and [libretro](https://www.libretro.com/), and now it
gaming performance with [WebRTC](https://github.com/pion/webrtc/) and [Libretro](https://www.libretro.com/), and now it
aims to deliver the most modern and convenient gaming experience through the technology.
Theoretically, in cloud gaming, games are run on remote servers and media are streamed to the player optimally to ensure
the most comfortable user interaction. It opens the ability to play any retro games on web-browser directly, which are
fully compatible with multi-platform like Desktop, Android, ~~IOS~~.
## Try the service at **[cloudretro.io](https://cloudretro.io)**
Direct play an existing
game: **[Pokemon Emerald](https://cloudretro.io/?id=4a5073a4b05ad0fe___Pokemon%20-%20Emerald%20Version%20(U))**
In ideal network condition and less resource contention on servers, the game will run smoothly as in the video demo.
Because I only hosted the platform on limited servers in US East, US West, Eu, Singapore, you may experience some
latency issues + connection problem. You can try hosting the service following the instruction the next section to have
a better sense of performance.
| Screenshot | Screenshot |
| :--------------------------------------------: | :--------------------------------------------: |
| ![screenshot](docs/img/landing-page-ps-hm.png) | ![screenshot](docs/img/landing-page-ps-x4.png) |
| ![screenshot](docs/img/landing-page-gb.png) | ![screenshot](docs/img/landing-page-front.png) |
## Feature
1. **Cloud gaming**: Game logic and storage is hosted on cloud service. It reduces the cumbersome of game
@ -63,23 +53,27 @@ a better sense of performance.
## Development environment
* Install Golang https://golang.org/doc/install.
* Install [Go](https://golang.org/doc/install)
* Install [libvpx](https://www.webmproject.org/code/), [libx264](https://www.videolan.org/developers/x264.html)
, [libopus](http://opus-codec.org/), [pkg-config](https://www.freedesktop.org/wiki/Software/pkg-config/)
, [sdl2](https://wiki.libsdl.org/Installation)
, [sdl2](https://wiki.libsdl.org/Installation), [libyuv](https://chromium.googlesource.com/libyuv/libyuv/)+[libjpeg-turbo](https://github.com/libjpeg-turbo/libjpeg-turbo)
```
# Ubuntu / Windows (WSL2)
apt-get install -y make gcc pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev
apt-get install -y make gcc pkg-config libvpx-dev libx264-dev libopus-dev libsdl2-dev libyuv-dev libjpeg-turbo8-dev libspeexdsp-dev
# MacOS
brew install pkg-config libvpx x264 opus sdl2
brew install pkg-config libvpx x264 opus sdl2 jpeg-turbo speexdsp
# Windows (MSYS2)
pacman -Sy --noconfirm --needed git make mingw-w64-x86_64-{gcc,pkgconf,dlfcn,libvpx,opus,x264-git,SDL2}
pacman -Sy --noconfirm --needed git make mingw-w64-ucrt-x86_64-{gcc,pkgconf,dlfcn,libvpx,opus,libx264,SDL2,libyuv,libjpeg-turbo,speexdsp}
```
(You don't need to download libyuv on macOS)
(If you need to use the app on an older version of Ubuntu that does not have libyuv (when it says: unable to locate package libyuv-dev), you can add a custom apt repository:
`add sudo add-apt-repository ppa:savoury1/graphics`)
Because the coordinator and workers need to run simultaneously. Workers connect to the coordinator.
1. Script
@ -96,38 +90,33 @@ Because the coordinator and workers need to run simultaneously. Workers connect
__Additionally, you may install and configure an `X Server` display in order to be able to run OpenGL cores.__
__See the `docker-compose.yml` file for Xvfb example config.__
__Minimum supported libx264 (x264 codec) version is v160!__
## Run with Docker
Use makefile script: `make dev.run-docker` or Docker Compose directly: `docker-compose up --build`
(`CLOUD_GAME_GAMES_PATH` is env variable for games on your host). It will spawn a docker environment and you can access
the service on `localhost:8000`.
Use makefile script: `make dev.run-docker` or Docker Compose directly: `docker compose up --build`.
It will spawn a docker environment and you can access the service on `localhost:8000`.
## 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.
The default configuration file is stored in the [`pkg/configs/config.yaml`](pkg/config/config.yaml) file.
This configuration file will be embedded into the applications and loaded automatically during startup.
In order to change the default parameters you can specify environment variables with the `CLOUD_GAME_` prefix, or place
a custom `config.yaml` file into one of these places: just near the application, `.cr` folder in user's home, or
specify own directory with `-w-conf` application param (`worker -w-conf /usr/conf`).
## Deployment
See an example of [deployment scripts](.github/workflows/cd) if you want to try to host your own cloud-retro copy in the cloud.
This script (deploy-app.sh) allows pushing configured application to the group of servers automatically.
The cloud server should be any Debian-based system with the docker-compose application [installed](https://docs.docker.com/compose/install/).
See an example of [deployment scripts](.github/workflows/cd) if you want to try to host your own cloud-retro copy in the
cloud. This script (deploy-app.sh) allows pushing configured application to the group of servers automatically. The
cloud server should be any Debian-based system with the docker-compose
application [installed](https://docs.docker.com/compose/install/).
## Technical documents
- [Design document v2](DESIGNv2.md)
- [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)
- [Code Pointer Wiki](https://github.com/giongto35/cloud-game/wiki/Code-Deep-Dive)
| High level | Worker internal |
| :----------------------------------: | :-----------------------------------------: |
| ![screenshot](docs/img/overview.png) | ![screenshot](docs/img/worker-internal.png) |
## FAQ
- [FAQ](https://github.com/giongto35/cloud-game/wiki/FAQ)
@ -136,7 +125,7 @@ The cloud server should be any Debian-based system with the docker-compose appli
By clicking these deep link, you can join the game directly and play it together with other people.
- [Play Pokemon Emerald](https://cloudretro.io/?id=652e45d78d2b91cd%7CPokemon%20-%20Emerald%20Version%20%28U%29)
- [Play Pokemon Emerald](https://cloudretro.io/?id=652e45d78d2b91cd___Pokemon%20-%20Emerald%20Version%20(U))
- [Fire Emblem](https://cloudretro.io/?id=314ea4d7f9c94d25___Fire%20Emblem%20%28U%29%20%5B%21%5D)
- [Samurai Showdown 4](https://cloudretro.io/?id=733c73064c368832___samsho4)
- [Metal Slug X](https://cloudretro.io/?id=2a9c4b3f1c872d28___mslugx)
@ -144,11 +133,6 @@ By clicking these deep link, you can join the game directly and play it together
And you can host the new game by yourself by accessing [cloudretro.io](https://cloudretro.io) and click "share" button
to generate a permanent link to your game.
<p align="center">
<img width="420" height="300" src="docs/img/multiplatform.png"> <br>
Synchronize a game session on multiple devices
</p>
## Credits
We are very much thankful to [everyone](https://github.com/giongto35/cloud-game/graphs/contributors) we've been lucky to
@ -158,7 +142,8 @@ Thanks:
* [Pion](https://github.com/pion) team for the incredible Golang WebRTC library and their support.
* [Libretro](https://www.libretro.com) team for the greatest emulation lib.
* [kivutar](https://github.com/kivutar) for [go-nanoarch](https://github.com/libretro/go-nanoarch), [ludo](https://github.com/libretro/ludo), and all.
* [kivutar](https://github.com/kivutar) for [go-nanoarch](https://github.com/libretro/go-nanoarch)
and [ludo](https://github.com/libretro/ludo).
* [gen2brain](https://github.com/gen2brain) for the [h264](https://github.com/gen2brain/x264-go) and VPX encoder.
* [poi5305](https://github.com/poi5305) for the [YUV video encoding](https://github.com/poi5305/go-yuv2webRTC).
* [fogleman](https://github.com/fogleman) for the [NES emulator](https://github.com/fogleman/nes).
@ -171,13 +156,19 @@ Thanks:
* [Linear Video game controller background Gadgets seamless pattern](https://stock.adobe.com/ru/images/linear-video-game-controller-background-gadgets-seamless-pattern/241143639)
by [Anna](https://stock.adobe.com/contributor/208277224/anna)
# Announcement
**[CloudMorph](https://github.com/giongto35/cloud-morph) is a sibling project that offers a more generic to
run any offline games/application on browser in Cloud Gaming
approach: [https://github.com/giongto35/cloud-morph](https://github.com/giongto35/cloud-morph))**
## Team
Authors:
- Nguyen Huu Thanh (https://www.linkedin.com/in/huuthanhnguyen)
- Tri Dang Minh (https://trich.im)
Maintainers:
- Sergey Stepanov (https://github.com/sergystepanov)

View file

@ -1,53 +0,0 @@
mupen64plus-169screensize = 480x270
mupen64plus-43screensize = 320x240
mupen64plus-alt-map = False
mupen64plus-aspect = 4:3
mupen64plus-astick-deadzone = 15
mupen64plus-astick-sensitivity = 100
mupen64plus-BackgroundMode = OnePiece
mupen64plus-BilinearMode = standard
mupen64plus-CorrectTexrectCoords = Off
mupen64plus-CountPerOp = 0
mupen64plus-cpucore = dynamic_recompiler
mupen64plus-CropMode = Auto
mupen64plus-d-cbutton = C3
mupen64plus-EnableCopyColorToRDRAM = Off
mupen64plus-EnableCopyDepthToRDRAM = Software
mupen64plus-EnableEnhancedHighResStorage = False
mupen64plus-EnableEnhancedTextureStorage = False
mupen64plus-EnableFBEmulation = True
mupen64plus-EnableFragmentDepthWrite = False
mupen64plus-EnableHWLighting = False
mupen64plus-EnableLegacyBlending = True
mupen64plus-EnableLODEmulation = True
mupen64plus-EnableNativeResTexrects = Disabled
mupen64plus-EnableOverscan = Enabled
mupen64plus-EnableShadersStorage = True
mupen64plus-EnableTextureCache = True
mupen64plus-ForceDisableExtraMem = False
mupen64plus-FrameDuping = False
mupen64plus-Framerate = Original
mupen64plus-FXAA = 0
mupen64plus-l-cbutton = C2
mupen64plus-MaxTxCacheSize = 8000
mupen64plus-NoiseEmulation = True
mupen64plus-OverscanBottom = 0
mupen64plus-OverscanLeft = 0
mupen64plus-OverscanRight = 0
mupen64plus-OverscanTop = 0
mupen64plus-pak1 = memory
mupen64plus-pak2 = none
mupen64plus-pak3 = none
mupen64plus-pak4 = none
mupen64plus-r-cbutton = C1
mupen64plus-rdp-plugin = gliden64
mupen64plus-rsp-plugin = hle
mupen64plus-rspmode = HLE
mupen64plus-txCacheCompression = True
mupen64plus-txEnhancementMode = None
mupen64plus-txFilterIgnoreBG = True
mupen64plus-txFilterMode = None
mupen64plus-txHiresEnable = False
mupen64plus-txHiresFullAlphaChannel = False
mupen64plus-u-cbutton = C4
mupen64plus-virefresh = Auto

Binary file not shown.

View file

@ -0,0 +1,2 @@
[autoexec]
ROGUE.EXE

BIN
assets/games/dos/rogue.zip Normal file

Binary file not shown.

Binary file not shown.

View file

@ -1,40 +1,32 @@
package main
import (
"context"
goflag "flag"
"math/rand"
"time"
config "github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
"github.com/giongto35/cloud-game/v2/pkg/coordinator"
"github.com/giongto35/cloud-game/v2/pkg/os"
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
"github.com/golang/glog"
flag "github.com/spf13/pflag"
"github.com/giongto35/cloud-game/v3/pkg/config"
"github.com/giongto35/cloud-game/v3/pkg/coordinator"
"github.com/giongto35/cloud-game/v3/pkg/logger"
"github.com/giongto35/cloud-game/v3/pkg/os"
)
var Version = ""
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
var Version = "?"
func main() {
conf := config.NewConfig()
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
conf, paths := config.NewCoordinatorConfig()
conf.ParseFlags()
logging.Init()
defer logging.Flush()
glog.Infof("[coordinator] version: %v", Version)
glog.V(4).Infof("Coordinator configs %v", conf)
c := coordinator.New(conf)
log := logger.NewConsole(conf.Coordinator.Debug, "c", false)
log.Info().Msgf("version %s", Version)
log.Info().Msgf("conf: v%v, loaded: %v", conf.Version, paths)
if log.GetLevel() < logger.InfoLevel {
log.Debug().Msgf("conf: %+v", conf)
}
c, err := coordinator.New(conf, log)
if err != nil {
log.Error().Err(err).Msgf("init fail")
return
}
c.Start()
ctx, cancelCtx := context.WithCancel(context.Background())
defer c.Shutdown(ctx)
<-os.ExpectTermination()
cancelCtx()
if err := c.Stop(); err != nil {
log.Error().Err(err).Msg("shutdown fail")
}
}

BIN
cmd/worker/default.pgo Normal file

Binary file not shown.

View file

@ -1,45 +1,40 @@
package main
import (
"context"
goflag "flag"
"math/rand"
"time"
config "github.com/giongto35/cloud-game/v2/pkg/config/worker"
"github.com/giongto35/cloud-game/v2/pkg/os"
"github.com/giongto35/cloud-game/v2/pkg/thread"
"github.com/giongto35/cloud-game/v2/pkg/util/logging"
"github.com/giongto35/cloud-game/v2/pkg/worker"
"github.com/golang/glog"
flag "github.com/spf13/pflag"
"github.com/giongto35/cloud-game/v3/pkg/config"
"github.com/giongto35/cloud-game/v3/pkg/logger"
"github.com/giongto35/cloud-game/v3/pkg/os"
"github.com/giongto35/cloud-game/v3/pkg/worker"
"github.com/giongto35/cloud-game/v3/pkg/worker/thread"
)
var Version = ""
func init() {
rand.Seed(time.Now().UTC().UnixNano())
}
var Version = "?"
func run() {
conf := config.NewConfig()
flag.CommandLine.AddGoFlagSet(goflag.CommandLine)
conf, paths := config.NewWorkerConfig()
conf.ParseFlags()
logging.Init()
defer logging.Flush()
log := logger.NewConsole(conf.Worker.Debug, "w", false)
log.Info().Msgf("version %s", Version)
log.Info().Msgf("conf: v%v, loaded: %v", conf.Version, paths)
if log.GetLevel() < logger.InfoLevel {
log.Debug().Msgf("conf: %+v", conf)
}
glog.Infof("[worker] version: %v", Version)
glog.V(4).Infof("[worker] Local configuration %+v", conf)
wrk := worker.New(conf)
wrk.Start()
ctx, cancelCtx := context.WithCancel(context.Background())
defer wrk.Shutdown(ctx)
<-os.ExpectTermination()
cancelCtx()
done := os.ExpectTermination()
w, err := worker.New(conf, log)
if err != nil {
log.Error().Err(err).Msgf("init fail")
return
}
w.Start(done)
<-done
time.Sleep(100 * time.Millisecond) // hack
if err := w.Stop(); err != nil {
log.Error().Err(err).Msg("shutdown fail")
}
}
func main() {
thread.MainWrapMaybe(run)
}
func main() { thread.Wrap(run) }

View file

@ -1,221 +0,0 @@
#
# Application configuration file
#
# application environment (dev, staging, prod)
# deprecated
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
# HTTP(S) server config
server:
address: :8000
https: false
# Letsencrypt or self cert config
tls:
address: :443
# allowed host name
domain:
# if both are set then will use certs
# and Letsencryt instead
httpsCert:
httpsKey:
analytics:
inject: false
gtag:
worker:
network:
# a coordinator address to connect to
coordinatorAddress: localhost:8000
# where to connect
endpoint: /wso
# ping endpoint
pingEndpoint: /echo
# set public ping address (IP or hostname)
publicAddress:
# make coordinator connection secure (wss)
secure: false
# ISO Alpha-2 country code to group workers by zones
zone:
monitoring:
# monitoring server port
port: 6602
profilingEnabled: false
# monitoring server URL prefix
metricEnabled: false
urlPrefix: /worker
server:
address: :9000
https: false
tls:
address: :444
# LetsEncrypt config
# allowed host name
domain:
# Own certs config
httpsCert:
httpsKey:
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
# save directory for emulator states
# special tag {user} will be replaced with current user's home dir
storage: "{user}/.cr/save"
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
# external cross-process mutex lock
extLock: "{user}/.cr/cloud-game.lock"
main:
type: buildbot
url: https://buildbot.libretro.com/nightly
# if repo has file compression
compression: zip
# a secondary repo to use i.e. for not found in the main cores
secondary:
type: github
url: https://github.com/sergystepanov/libretro-spiegel/blob/main
compression: zip
# Libretro core configuration
#
# The emulator selection will happen in this order:
# - based on the folder name in the folder param
# - based on the folder name (core name) in the list (i.e. nes, snes)
# - based on the rom names in the roms param
#
# Available config params:
# - lib (string)
# - config (string)
# - roms ([]string)
# - folder (string)
# By default emulator selection is based on the folder named as cores
# in the list (i.e. nes, snes) but if you specify folder param,
# then it will try to load the ROM file from that folder first.
# - width (int) -- broken
# - height (int) -- broken
# - ratio (float)
# - isGlAllowed (bool)
# - usesLibCo (bool)
# - hasMultitap (bool)
list:
gba:
lib: mgba_libretro
roms: [ "gba", "gbc" ]
pcsx:
lib: pcsx_rearmed_libretro
roms: [ "cue" ]
# example of folder override
folder: psx
# MAME core requires additional manual setup, please read:
# https://docs.libretro.com/library/fbneo/
mame:
lib: fbneo_libretro
roms: [ "zip" ]
nes:
lib: nestopia_libretro
roms: [ "nes" ]
snes:
lib: snes9x_libretro
roms: [ "smc", "sfc", "swc", "fig", "bs" ]
hasMultitap: true
n64:
lib: mupen64plus_next_libretro
config: mupen64plus_next_libretro.cfg
roms: [ "n64", "v64", "z64" ]
isGlAllowed: true
usesLibCo: true
encoder:
audio:
channels: 2
# audio frame duration needed for WebRTC (Opus)
frame: 20
frequency: 48000
video:
# h264, vpx (VP8)
codec: h264
# see: https://trac.ffmpeg.org/wiki/Encode/H.264
h264:
# Constant Rate Factor (CRF) 0-51 (default: 23)
crf: 17
# ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
preset: veryfast
# baseline, main, high, high10, high422, high444
profile: main
# film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
tune: zerolatency
# 0-3
logLevel: 0
# see: https://www.webmproject.org/docs/encoder-parameters
vpx:
# target bitrate (KBit/s)
bitrate: 1200
# force keyframe interval
keyframeInterval: 5
# run without a game
# (experimental)
withoutGame: false
webrtc:
# turn off default Pion interceptors for performance reasons
# (experimental)
disableDefaultInterceptors:
# a list of STUN/TURN servers for the client
iceServers:
- url: stun:stun.l.google.com:19302
# instead of random unlimited port range for
# WebRTC UDP connections, these params
# define ICE candidates port range explicitly
icePorts:
min:
max:
# override ICE candidate IP, see: https://github.com/pion/webrtc/issues/835,
# can be used for Docker bridged network internal IP override
iceIpMap:

View file

@ -1,21 +1,33 @@
version: '3'
services:
cloud-game:
build: .
image: cloud-game-local
container_name: cloud-game-local
privileged: true
environment:
- DISPLAY=:99
- MESA_GL_VERSION_OVERRIDE=3.3
network_mode: "host"
- MESA_GL_VERSION_OVERRIDE=4.5
- CLOUD_GAME_WEBRTC_SINGLEPORT=8443
# - CLOUD_GAME_WEBRTC_ICEIPMAP=127.0.0.1
- CLOUD_GAME_COORDINATOR_DEBUG=true
- CLOUD_GAME_WORKER_DEBUG=true
# - PION_LOG_TRACE=all
ports:
- "8000:8000"
- "9000:9000"
- "8443:8443/udp"
command: >
bash -c "Xvfb :99 & coordinator --v=5 & worker --coordinatorhost localhost:8000"
bash -c "./coordinator & ./worker"
volumes:
# keep cores persistent in the cloud-game_cores volume
- cores:/usr/local/share/cloud-game/assets/cores
- ${CLOUD_GAME_GAMES_PATH}:/usr/local/share/cloud-game/assets/games
- ./assets/cores:/usr/local/share/cloud-game/assets/cores
- ./assets/games:/usr/local/share/cloud-game/assets/games
- x11:/tmp/.X11-unix
xvfb:
image: kcollins/xvfb:latest
volumes:
- x11:/tmp/.X11-unix
command: [ ":99", "-screen", "0", "320x240x16" ]
volumes:
cores:
x11:

View file

@ -1,47 +0,0 @@
## Streaming process description
This document describes the step-by-step process of media streaming in all parts of the application.
```
┌──────────────┐ ┌───────────────┐ ┌──────────────┐
│ USER AGENT │ │ COORDINATOR │ │ WORKER...n │
├──────────────┤ ├───────────────┤ ├──────────────┤
│ TCP/WS ├──1──►│ WS ──────► WS │◄───┤ TCP/WS │
│ │ │ ▲ 2 │ │ │ │
│ │ │ └───────────┘ │ │ │
│ │ └───────────────┘ │ │
│ UDP/RTP │◄─────────────3────────────┤ UDP/RTP │
│ AUDIO < │ OPUS │ AUDIO │
│ VIDEO < │ VP8/H264 │ VIDEO │
│ DATA > │ 010101 │ DATA │
└──────────────┘ └──────────────┘
```
The app is based on WebRTC technology which allows the server to stream media and exchange data with ultra-low latencies. An essential part of these types of P2P connections is the signaling process. It's implemented as a custom text-based messaging protocol on top of WebSocket (quite similarly to [WAMP](https://wamp-proto.org)). The app supports both STUN and TURN protocols for NAT traversal or ICE. In terms of supported codecs, it can stream h264, VP8, and OPUS media.
The streaming process begins when a user opens the main application page (index.html) served by the coordinator.
- The user's browser tries to open a new WebSocket connection to the coordinator — socket.init(roomId, zone) [web/js/network/socket.js:32](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L32)
> In the initial WebSocket Upgrade request query it may send two params: roomId — an identification number for existing game rooms stored in the URL query of the application page (i.e. app.com/?id=xxxxxx), zone — or, more precisely, region — serves the purpose of CDN and geographical segmentation of the streaming.
- On the coordinator side this request goes into a dedicated handler (/ws) — func (o *Server) WS(w http.ResponseWriter, r *http.Request) [pkg/coordinator/handlers.go:150](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/pkg/coordinator/handlers.go#L150)
- There, it unconditionally accepts the WebSocket connection and tags it with some ID, so it will be listening to messages from the user's side. Here a new client connection should be considered as established.
- Next, given provided query params, the coordinator tries to find a suitable worker whose job — directly stream games to a user.
> This process of choosing the right worker is following: if there is no roomId param, then the coordinator gathers the full list of available workers, filters them by a zone value (if provided), returns the user a list of public URLs, which he can ping and send results back to the coordinator. After that, the coordinator links the fastest one with the user. Alternatively, if the user did provide some roomId, then the coordinator directly assigns a worker with that room (workers have 1:1 mapping to rooms or games).
> All the information exchange initiated from the worker side is handled in a separate endpoint (/wso) [pkg/coordinator/handlers.go#L81](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/handlers.go#L81).
- Coordinator sends to the user ICE servers and the list of games available for playing. That's handled in [web/js/network/socket.js:57](https://github.com/giongto35/cloud-game/blob/ae5260fb4726fd34cc0b0b05100dcc8457f52883/web/js/network/socket.js#L57).
- From this point, the user's browser begins to initialize WebRTC connection to the worker — web/js/controller.js:413 → [web/js/network/rtcp.js:16](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L16).
- First, it sends init request through the WebSocket connection to the coordinator handler in [pkg/coordinator/useragenthandlers.go:17](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go#L17).
> Following a standard WebRTC call [negotiation procedure](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Signaling_and_video_calling), the coordinator acts as a mediator between users and workers. The signaling protocol here is a text messaging through WebSocket transport.
- Coordinator notifies the user's worker that it wants to establish a new PeerConnection (call). That part is being handled in [pkg/worker/internalhandlers.go:42](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/worker/internalhandlers.go#L42). It is worth noting that it is a worker who makes SDP offer and waits for an SDP answer.
- Worker initializes new WebRTC connection handler in func (w *WebRTC) StartClient(isMobile bool, iceCB OnIceCallback) (string, error) [pkg/webrtc/webrtc.go:103](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/webrtc/webrtc.go#L103).
- Then through the coordinator it makes simultaneously an SDP offer as well as sends ICE candidates that are handled on the coordinator side (from the user) in [pkg/coordinator/useragenthandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/useragenthandlers.go),
(from the worker) in [pkg/coordinator/internalhandlers.go](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/pkg/coordinator/internalhandlers.go), and on the user side both in [web/js/network/socket.js:56](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/socket.js#L56) and inside [web/js/network/rtcp.js](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js).
- Browser on the user's side after SDP offer links remote streams to the HTML Video element in [web/js/controller.js:417](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L417), makes SDP answer and gathers remote ICE candidates until it's done (if receive an empty ICE candidate).
- For the user's side a successful WebRTC connection should be considered established when WebRTC datachannel is opened here [web/js/network/rtcp.js:31](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/network/rtcp.js#L31).
*And that should be it for the streaming part.*
> At this point all the connections should be successfully established and the user's ready for a game to start. The coordinator should notify the worker about that fact and the worker starts pushing media frames, listen to the input through the direct to the user WebRTC data channel.
- Then the user may send the game start request to the coordinator in [web/js/controller.js:153](https://github.com/giongto35/cloud-game/blob/a7d8e53dac2bbcf8306e0dafe3878644c760d368/web/js/controller.js#L153).
#### Streaming requirements
- Workers should not have any closed UDP ports to be able to provide suitable ICE candidates.
- Coordinator should have at least one non-blocked TCP port (default: 8000) for HTTP/WebSocket signaling connections from users and workers.
- Browser should not block WebRTC and support it (check [here](https://test.webrtc.org/)).

View file

@ -1,25 +0,0 @@
# Web-based Cloud Gaming Service Design Document
Web-based Cloud Gaming Service contains multiple workers for gaming stream and a coordinator (Coordinator) for distributing traffic and pairing up connection.
## Worker
Worker is responsible for streaming game to frontend
![worker](../img/worker.png)
- After Coordinator matches the most appropriate server to the user, webRTC peer-to-peer handshake will be conducted. The coordinator will exchange the signature (WebRTC Session Remote Description) between two peers over Web Socket connection.
- On worker, each user session will spawn a new room running a gaming emulator. Image stream and audio stream from emulator is captured and encoded to WebRTC streaming format. We applied Vp8 for Video compression and Opus for audio compression to ensure the smoothest experience. After finish encoded, these stream is then piped out to user and observers joining that room.
- On the other hand, input from users is sent to workers over WebRTC DataChannel. Game logic on the emulator will be updated based on the input stream.
- Game state is stored in cloud storage, so all workers can collaborate and keep the same understanding with each other. It allows user can continue from the saved state in the next time.
## Coordinator
Coordinator is loadbalancer and coordinator, which is in charge of picking the most suitable workers for a user. Every time a user connects to Coordinator, it will collect all the metric from all workers, i.e free CPU resources and latency from worker to user. Coordinator will decide the best candidate based on the metric and setup peer-to-peer connection between worker and user based on WebRTC protocol
![Architecture](../img/coordinator.png)
1. A user connected to Coordinator .
2. Coordinator will find the most suitable worker to serve the user.
3. Coordinator collects all latencies from workers to users as well as CPU usage on each machine.
4. Coordinator setup peer-to-peer handshake between worker and user by exchanging Session Description Protocol.
5. A game is hosted on worker and streamed to the user.

View file

@ -1,43 +0,0 @@
# Web-based Cloud Gaming Service Implementation Document
## Code structure
```
.
├── cmd: service entrypoint
│ ├── main.go: Spawn coordinator or worker based on flag
│ └── main_test.go
├── static: static file for front end
│ ├── js
│ │ └── ws.js: client logic
│ ├── game.html: frontend with gameboy ui
│ └── index_ws.html: raw frontend without ui
├── coordinator: coordinator
│ ├── handlers.go: coordinator entrypoint
│ ├── browser.go: router listening to browser
│ └── worker.go: router listening to worker
├── games: roms list, no code logic
├── worker: integration between emulator + webrtc (communication)
│ ├── room:
│ │ ├── room.go: room logic
│ │ └── media.go: video + audio encoding
│ ├── handlers.go: worker entrypoint
│ └── coordinator.go: router listening to coordinator
├── emulator: emulator internal
│ ├── nes: NES device internal
│ ├── director.go: coordinator of views
│ └── gameview.go: in game logic
├── cws
│ └── cws.go: socket multiplexer library, used for signaling
└── webrtc: webrtc streaming logic
```
## Room
Room is a fundamental part of the system. Each user session will spawn a room with a game running inside. There is a pipeline to encode images and audio and stream them out from emulator to user. The pipeline also listens to all input and streams to the emulator.
## Worker
Worker is an instance that can be provisioned to scale up the traffic. There are multiple rooms inside a worker. Worker will listen to coordinator events in `coordinator.go`.
## Coordinator
Coordinator is the coordinator, which handles all communication with workers and frontend.
Coordinator will pair up a worker and a user for peer streaming. In WebRTC handshaking, two peers need to exchange their signature (Session Description Protocol) to initiate a peerconnection.
Events come from frontend will be handled in `coordinator/browser.go`. Events come from worker will be handled in `coordinator/worker.go`. Coordinator stays in the middle and relays handshake packages between workers and user.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View file

@ -1,28 +0,0 @@
# Web-based Cloud Gaming Service Game Instruction
The game can be played on Desktop, Mobile (Android only). You can plug joystick to play with the game.
Click question mark on the top left to see game instruction.
## Key map on Desktop
Game keymap follows
Arrow keys to move
H -> Show help
C -> Start
V -> Select
Z -> A
X -> B
S -> Save (Save state)
A -> Load (Load previous saved state)
W -> Share your running game to other or you can keep it to continue playing the next time. Multiple people can access the same game for multiplayer or observation.
F -> Full screen
Q -> Quit the current game and go to menu screen.**NOTE**: we are facing some issue with quit, so it's better to refresh the page.
## Mobile play
You can play the game on Android device. Make sure your Android has the version that support WebRTC. IOS doesn't support WebRTC streaming now.
The keys map are equivalent to Desktop. Press the button to fire input.
## Joystick
The game also accepts joystick, so you can try plug in one and experience. It will be very fun!

91
go.mod
View file

@ -1,37 +1,62 @@
module github.com/giongto35/cloud-game/v2
module github.com/giongto35/cloud-game/v3
go 1.13
go 1.25
require (
cloud.google.com/go v0.91.1 // indirect
cloud.google.com/go/storage v1.16.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/go-gl/gl v0.0.0-20210813123233-e4099ee2221f
github.com/gofrs/flock v0.8.1
github.com/gofrs/uuid v4.0.0+incompatible
github.com/golang/glog v0.0.0-20210429001901-424d2337a529
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/gorilla/websocket v1.4.2
github.com/kkyr/fig v0.3.0
github.com/pion/ice/v2 v2.1.12 // indirect
github.com/pion/interceptor v0.0.15
github.com/pion/rtp v1.7.1
github.com/pion/srtp/v2 v2.0.5 // indirect
github.com/pion/webrtc/v3 v3.0.32
github.com/prometheus/client_golang v1.11.0
github.com/prometheus/common v0.30.0 // indirect
github.com/prometheus/procfs v0.7.2 // indirect
github.com/spf13/pflag v1.0.5
github.com/veandco/go-sdl2 v0.4.8
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
golang.org/x/mod v0.5.0 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 // indirect
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/api v0.54.0 // indirect
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d // indirect
github.com/VictoriaMetrics/metrics v1.40.2
github.com/cavaliergopher/grab/v3 v3.0.1
github.com/fsnotify/fsnotify v1.9.0
github.com/goccy/go-json v0.10.5
github.com/gofrs/flock v0.13.0
github.com/gorilla/websocket v1.5.3
github.com/knadh/koanf/maps v0.1.2
github.com/knadh/koanf/v2 v2.3.0
github.com/minio/minio-go/v7 v7.0.97
github.com/pion/ice/v4 v4.1.0
github.com/pion/interceptor v0.1.42
github.com/pion/logging v0.2.4
github.com/pion/webrtc/v4 v4.1.8
github.com/rs/xid v1.6.0
github.com/rs/zerolog v1.34.0
github.com/veandco/go-sdl2 v0.4.40
golang.org/x/crypto v0.46.0
golang.org/x/image v0.34.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.9 // indirect
github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.16 // indirect
github.com/pion/rtp v1.8.27 // indirect
github.com/pion/sctp v1.8.41 // indirect
github.com/pion/sdp/v3 v3.0.17 // indirect
github.com/pion/srtp/v3 v3.0.9 // indirect
github.com/pion/stun/v3 v3.0.2 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pion/turn/v4 v4.1.3 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
)

879
go.sum
View file

@ -1,762 +1,133 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=
cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=
cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=
cloud.google.com/go v0.88.0/go.mod h1:dnKwfYbP9hQhefiUvpbcAyoGSHUrOxR20JVElLiUvEY=
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.91.1 h1:w+u8ttN/QtYrpvgXNUd2G6kwqrqCIQbkINlXQjHP1ek=
cloud.google.com/go v0.91.1/go.mod h1:V358WZfbFQkmC3gv5XCxzZq2e3h7OGvQR0IXtj77ylI=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.16.0 h1:1UwAux2OZP4310YXg5ohqBEpV16Y93uZG4+qOX7K2Kg=
cloud.google.com/go/storage v1.16.0/go.mod h1:ieKBmUyzcftN5tbxwnXClMKH00CfcQ+xL6NN0r5QfmE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec h1:4XvMn0XuV7qxCH22gbnR79r+xTUaLOSA0GW/egpO3SQ=
github.com/cavaliercoder/grab v1.0.1-0.20201108051000-98a5bfe305ec/go.mod h1:NbXoa59CCAGqtRm7kRrcZIk2dTCJMRVF8QI3BOD7isY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/VictoriaMetrics/metrics v1.40.2 h1:OVSjKcQEx6JAwGeu8/KQm9Su5qJ72TMEW4xYn5vw3Ac=
github.com/VictoriaMetrics/metrics v1.40.2/go.mod h1:XE4uudAAIRaJE614Tl5HMrtoEU6+GDZO4QTnNSsZRuA=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q=
github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU=
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210715191844-86eeefc3e471/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210804190019-f964ff605595/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkyr/fig v0.3.0 h1:5bd1amYKp/gsK2bGEUJYzcCrQPKOZp6HZD9K21v9Guo=
github.com/kkyr/fig v0.3.0/go.mod h1:fEnrLjwg/iwSr8ksJF4DxrDmCUir5CaVMLORGYMcz30=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pion/datachannel v1.4.21 h1:3ZvhNyfmxsAqltQrApLPQMhSFNA+aT87RqyCq4OXmf0=
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/dtls/v2 v2.0.9 h1:7Ow+V++YSZQMYzggI0P9vLJz/hUFcffsfGMfT/Qy+u8=
github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho=
github.com/pion/ice/v2 v2.1.10/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0=
github.com/pion/ice/v2 v2.1.12 h1:ZDBuZz+fEI7iDifZCYFVzI4p0Foy0YhdSSZ87ZtRcRE=
github.com/pion/ice/v2 v2.1.12/go.mod h1:ovgYHUmwYLlRvcCLI67PnQ5YGe+upXZbGgllBDG/ktU=
github.com/pion/interceptor v0.0.13/go.mod h1:svsW2QoLHLoGLUr4pDoSopGBEWk8FZwlfxId/OKRKzo=
github.com/pion/interceptor v0.0.15 h1:pQFkBUL8akUHiGoFr+pM94Q/15x7sLFh0K3Nj+DCC6s=
github.com/pion/interceptor v0.0.15/go.mod h1:pg3J253eGi5bqyKzA74+ej5Y19ez2jkWANVnF+Z9Dfk=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.97 h1:lqhREPyfgHTB/ciX8k2r8k0D93WaFqxbJX36UZq5occ=
github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohwdaSmPPSRSk=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=
github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=
github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=
github.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=
github.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=
github.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=
github.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.6 h1:1zvwBbyd0TeEuuWftrd/4d++m+/kZSeiguxU61LFWpo=
github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0=
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/rtp v1.7.1 h1:hCaxfVgPGt13eF/Tu9RhVn04c+dAcRZmhdDWqUE13oY=
github.com/pion/rtp v1.7.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sctp v1.7.12 h1:GsatLufywVruXbZZT1CKg+Jr8ZTkwiPnmUC/oO9+uuY=
github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8=
github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk=
github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0=
github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ=
github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3 h1:vdBfvfU/0Wq8kd2yhUMSDB/x+O4Z9MYVl2fJ5BT4JZw=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v3 v3.0.32 h1:5J+zNep9am8Swh6kEMp+LaGXNvn6qQWpGkLBnVW44L4=
github.com/pion/webrtc/v3 v3.0.32/go.mod h1:wX3V5dQQUGCifhT1mYftC2kCrDQX6ZJ3B7Yad0R9JK0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/rtp v1.8.27 h1:kbWTdZr62RDlYjatVAW4qFwrAu9XcGnwMsofCfAHlOU=
github.com/pion/rtp v1.8.27/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=
github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/common v0.30.0 h1:JEkYlQnpzrzQFxi6gnukFPdQ+ac82oRhzMcIduJu/Ug=
github.com/prometheus/common v0.30.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.2 h1:zE6zJjRS9S916ptrZ326OU0++1XRwHgxkvCFflxx6Fo=
github.com/prometheus/procfs v0.7.2/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/veandco/go-sdl2 v0.4.8 h1:A26KeX6R1CGt/BQGEov6oxYmVGMMEWDVqTvK1tXvahE=
github.com/veandco/go-sdl2 v0.4.8/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e h1:VvfwVmMH40bpMeizC9/K7ipM5Qjucuu16RWfneFPyhQ=
golang.org/x/crypto v0.0.0-20210813211128-0a44fdfbc16e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0 h1:UG21uOlmZabA4fW5i7ZX6bjw1xELEGg/ZLgZq9auk/Q=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210615190721-d04028783cf1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5 h1:Ati8dO7+U7mxpkPSxBZQEvzHVUYB/MqCklCN8ig5w/o=
golang.org/x/oauth2 v0.0.0-20210810183815-faf39c7919d5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71 h1:ikCpsnYR+Ew0vu99XlDp55lGgDJdIMx3f4a18jfse/s=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=
google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=
google.golang.org/api v0.49.0/go.mod h1:BECiH72wsfwUvOVn3+btPD5WHi0LzavZReBndi42L18=
google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=
google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=
google.golang.org/api v0.52.0/go.mod h1:Him/adpjt0sxtkWViy0b6xyKW/SD71CwdJ7HqJo7SrU=
google.golang.org/api v0.54.0 h1:ECJUVngj71QI6XEm7b1sAf8BljU5inEhMbKPR8Lxhhk=
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210624174822-c5cf32407d0a/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=
google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=
google.golang.org/genproto v0.0.0-20210721163202-f1cecdd8b78a/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210722135532-667f2b7c528f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=
google.golang.org/genproto v0.0.0-20210811021853-ddbe55d93216/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d h1:fPtHPeysWvGVJwQFKu3B7H2DB2sOEsW7UTayKkWESKw=
google.golang.org/genproto v0.0.0-20210816143620-e15ff196659d/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/veandco/go-sdl2 v0.4.40 h1:fZv6wC3zz1Xt167P09gazawnpa0KY5LM7JAvKpX9d/U=
github.com/veandco/go-sdl2 v0.4.40/go.mod h1:OROqMhHD43nT4/i9crJukyVecjPNYYuCofep6SNiAjY=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

202
pkg/api/api.go Normal file
View file

@ -0,0 +1,202 @@
// Package api defines the general API for both coordinator and worker applications.
//
// Each API call (request and response) is a JSON-encoded "packet" of the following structure:
//
// id - (optional) a globally unique packet id;
// t - (required) one of the predefined unique packet types;
// p - (optional) packet payload with arbitrary data.
//
// The basic idea behind this API is that the packets differentiate by their predefined types
// with which it is possible to unwrap the payload into distinct request/response data structures.
// And the id field is used for tracking packets through a chain of different network points (apps, devices),
// for example, passing a packet from a browser forward to a worker and back through a coordinator.
//
// Example:
//
// {"t":4,"p":{"ice":[{"urls":"stun:stun.l.google.com:19302"}],"games":["Sushi The Cat"],"wid":"cfv68irdrc3ifu3jn6bg"}}
package api
import (
"encoding/json"
"fmt"
"strings"
)
type (
Id interface {
String() string
}
Stateful struct {
Id string `json:"id"`
}
Room struct {
Rid string `json:"room_id"`
}
StatefulRoom struct {
Id string `json:"id"`
Rid string `json:"room_id"`
}
PT uint8
)
type In[I Id] struct {
Id I `json:"id,omitempty"`
T PT `json:"t"`
Payload json.RawMessage `json:"p,omitempty"` // should be json.RawMessage for 2-pass unmarshal
}
func (i In[I]) GetId() I { return i.Id }
func (i In[I]) GetPayload() []byte { return i.Payload }
func (i In[I]) GetType() PT { return i.T }
type Out struct {
Id string `json:"id,omitempty"` // string because omitempty won't work as intended with arrays
T uint8 `json:"t"`
Payload any `json:"p,omitempty"`
}
func (o *Out) SetId(s string) { o.Id = s }
func (o *Out) SetType(u uint8) { o.T = u }
func (o *Out) SetPayload(a any) { o.Payload = a }
func (o *Out) SetGetId(s fmt.Stringer) { o.Id = s.String() }
func (o *Out) GetPayload() any { return o.Payload }
// Packet codes:
//
// x, 1xx - user codes
// 15x - webrtc data exchange codes
// 2xx - worker codes
const (
CheckLatency PT = 3
InitSession PT = 4
WebrtcInit PT = 100
WebrtcOffer PT = 101
WebrtcAnswer PT = 102
WebrtcIce PT = 103
StartGame PT = 104
QuitGame PT = 105
SaveGame PT = 106
LoadGame PT = 107
ChangePlayer PT = 108
RecordGame PT = 110
GetWorkerList PT = 111
ErrNoFreeSlots PT = 112
ResetGame PT = 113
RegisterRoom PT = 201
CloseRoom PT = 202
IceCandidate = WebrtcIce
TerminateSession PT = 204
AppVideoChange PT = 150
LibNewGameList PT = 205
PrevSessions PT = 206
)
func (p PT) String() string {
switch p {
case CheckLatency:
return "CheckLatency"
case InitSession:
return "InitSession"
case WebrtcInit:
return "WebrtcInit"
case WebrtcOffer:
return "WebrtcOffer"
case WebrtcAnswer:
return "WebrtcAnswer"
case WebrtcIce:
return "WebrtcIce"
case StartGame:
return "StartGame"
case ChangePlayer:
return "ChangePlayer"
case QuitGame:
return "QuitGame"
case SaveGame:
return "SaveGame"
case LoadGame:
return "LoadGame"
case RecordGame:
return "RecordGame"
case GetWorkerList:
return "GetWorkerList"
case ErrNoFreeSlots:
return "NoFreeSlots"
case ResetGame:
return "ResetGame"
case RegisterRoom:
return "RegisterRoom"
case CloseRoom:
return "CloseRoom"
case TerminateSession:
return "TerminateSession"
case AppVideoChange:
return "AppVideoChange"
case LibNewGameList:
return "LibNewGameList"
case PrevSessions:
return "PrevSessions"
default:
return "Unknown"
}
}
// Various codes
const (
EMPTY = ""
OK = "ok"
)
var (
ErrForbidden = fmt.Errorf("forbidden")
ErrMalformed = fmt.Errorf("malformed")
)
var (
EmptyPacket = Out{Payload: ""}
ErrPacket = Out{Payload: "err"}
OkPacket = Out{Payload: "ok"}
)
func Do[I Id, T any](in In[I], fn func(T)) error {
if dat := Unwrap[T](in.Payload); dat != nil {
fn(*dat)
return nil
}
return ErrMalformed
}
func DoE[I Id, T any](in In[I], fn func(T) error) error {
if dat := Unwrap[T](in.Payload); dat != nil {
return fn(*dat)
}
return ErrMalformed
}
func Unwrap[T any](data []byte) *T {
out := new(T)
if err := json.Unmarshal(data, out); err != nil {
return nil
}
return out
}
func UnwrapChecked[T any](bytes []byte, err error) (*T, error) {
if err != nil {
return nil, err
}
return Unwrap[T](bytes), nil
}
func Wrap(t any) ([]byte, error) { return json.Marshal(t) }
const separator = "___"
func ExplodeDeepLink(link string) (string, string) {
p := strings.SplitN(link, separator, 2)
if len(p) == 1 {
return p[0], ""
}
return p[0], p[1]
}

42
pkg/api/coordinator.go Normal file
View file

@ -0,0 +1,42 @@
package api
type (
CloseRoomRequest string
ConnectionRequest[T Id] struct {
Addr string `json:"addr,omitempty"`
Id T `json:"id,omitempty"`
IsHTTPS bool `json:"is_https,omitempty"`
PingURL string `json:"ping_url,omitempty"`
Port string `json:"port,omitempty"`
Tag string `json:"tag,omitempty"`
Zone string `json:"zone,omitempty"`
}
GetWorkerListResponse struct {
Servers []Server `json:"servers"`
}
RegisterRoomRequest string
)
const (
DataQueryParam = "data"
RoomIdQueryParam = "room_id"
ZoneQueryParam = "zone"
WorkerIdParam = "wid"
)
// Server contains a list of server groups.
// Server is a separate machine that may contain
// multiple sub-processes.
type Server struct {
Addr string `json:"addr,omitempty"`
Id Id `json:"id,omitempty"`
IsBusy bool `json:"is_busy,omitempty"`
InGroup bool `json:"in_group,omitempty"`
Machine string `json:"machine,omitempty"`
PingURL string `json:"ping_url"`
Port string `json:"port,omitempty"`
Replicas uint32 `json:"replicas,omitempty"`
Room string `json:"room,omitempty"`
Tag string `json:"tag,omitempty"`
Zone string `json:"zone,omitempty"`
}

36
pkg/api/user.go Normal file
View file

@ -0,0 +1,36 @@
package api
type (
ChangePlayerUserRequest int
CheckLatencyUserResponse []string
CheckLatencyUserRequest map[string]int64
GameStartUserRequest struct {
GameName string `json:"game_name"`
RoomId string `json:"room_id"`
Record bool `json:"record,omitempty"`
RecordUser string `json:"record_user,omitempty"`
PlayerIndex int `json:"player_index"`
}
GameStartUserResponse struct {
RoomId string `json:"roomId"`
Av *AppVideoInfo `json:"av"`
KbMouse bool `json:"kb_mouse"`
}
IceServer struct {
Urls string `json:"urls,omitempty"`
Username string `json:"username,omitempty"`
Credential string `json:"credential,omitempty"`
}
InitSessionUserResponse struct {
Ice []IceServer `json:"ice"`
Games []AppMeta `json:"games"`
Wid string `json:"wid"`
}
AppMeta struct {
Alias string `json:"alias,omitempty"`
Title string `json:"title"`
System string `json:"system"`
}
WebrtcAnswerUserRequest string
WebrtcUserIceCandidate string
)

70
pkg/api/worker.go Normal file
View file

@ -0,0 +1,70 @@
package api
type (
ChangePlayerRequest struct {
StatefulRoom
Index int `json:"index"`
}
ChangePlayerResponse int
GameQuitRequest StatefulRoom
LoadGameRequest StatefulRoom
LoadGameResponse string
ResetGameRequest StatefulRoom
ResetGameResponse string
SaveGameRequest StatefulRoom
SaveGameResponse string
StartGameRequest struct {
StatefulRoom
Record bool
RecordUser string
Game string `json:"game"`
PlayerIndex int `json:"player_index"`
}
GameInfo struct {
Alias string `json:"alias"`
Base string `json:"base"`
Name string `json:"name"`
Path string `json:"path"`
System string `json:"system"`
Type string `json:"type"`
}
StartGameResponse struct {
Room
AV *AppVideoInfo `json:"av"`
Record bool `json:"record"`
KbMouse bool `json:"kb_mouse"`
}
RecordGameRequest struct {
StatefulRoom
Active bool `json:"active"`
User string `json:"user"`
}
RecordGameResponse string
TerminateSessionRequest Stateful
WebrtcAnswerRequest struct {
Stateful
Sdp string `json:"sdp"`
}
WebrtcIceCandidateRequest struct {
Stateful
Candidate string `json:"candidate"` // Base64-encoded ICE candidate
}
WebrtcInitRequest Stateful
WebrtcInitResponse string
AppVideoInfo struct {
W int `json:"w"`
H int `json:"h"`
S int `json:"s"`
A float32 `json:"a"`
}
LibGameListInfo struct {
T int
List []GameInfo
}
PrevSessionInfo struct {
List []string
}
)

View file

@ -1,8 +0,0 @@
package codec
type VideoCodec string
const (
H264 VideoCodec = "h264"
VPX VideoCodec = "vpx"
)

121
pkg/com/com.go Normal file
View file

@ -0,0 +1,121 @@
package com
import "github.com/giongto35/cloud-game/v3/pkg/logger"
type stringer interface {
comparable
String() string
}
type NetClient[K stringer] interface {
Disconnect()
Id() K
}
type NetMap[K stringer, T NetClient[K]] struct{ Map[K, T] }
func NewNetMap[K stringer, T NetClient[K]]() NetMap[K, T] {
return NetMap[K, T]{Map: Map[K, T]{m: make(map[K]T, 10)}}
}
func (m *NetMap[K, T]) Add(client T) bool { return m.Put(client.Id(), client) }
func (m *NetMap[K, T]) Empty() bool { return m.Map.Len() == 0 }
func (m *NetMap[K, T]) Remove(client T) { m.Map.Remove(client.Id()) }
func (m *NetMap[K, T]) RemoveL(client T) int { return m.Map.RemoveL(client.Id()) }
func (m *NetMap[K, T]) Reset() { m.Map = Map[K, T]{m: make(map[K]T, 10)} }
func (m *NetMap[K, T]) RemoveDisconnect(client T) { client.Disconnect(); m.Remove(client) }
func (m *NetMap[K, T]) Find(id string) T {
v, _ := m.Map.FindBy(func(v T) bool {
return v.Id().String() == id
})
return v
}
type SocketClient[T ~uint8, P Packet[T], X any, P2 Packet2[X]] struct {
id Uid
rpc *RPC[T, P]
sock *Connection
log *logger.Logger // a special logger for showing x -> y directions
}
func NewConnection[T ~uint8, P Packet[T], X any, P2 Packet2[X]](conn *Connection, id Uid, log *logger.Logger) *SocketClient[T, P, X, P2] {
if id.IsNil() {
id = NewUid()
}
dir := logger.MarkOut
if conn.IsServer() {
dir = logger.MarkIn
}
dirClLog := log.Extend(log.With().
Str("cid", id.Short()).
Str(logger.DirectionField, dir),
)
dirClLog.Debug().Msg("Connect")
return &SocketClient[T, P, X, P2]{sock: conn, id: id, log: dirClLog}
}
func (c *SocketClient[T, P, _, _]) ProcessPackets(fn func(in P) error) chan struct{} {
c.rpc = NewRPC[T, P]()
c.rpc.Handler = func(p P) {
c.log.Debug().Str(logger.DirectionField, logger.MarkIn).Msgf("%v", p.GetType())
if err := fn(p); err != nil { // 3rd handler
c.log.Error().Err(err).Send()
}
}
c.sock.conn.SetMessageHandler(c.handleMessage) // 1st handler
return c.sock.conn.Listen()
}
func (c *SocketClient[T, P, X, P2]) SetErrorHandler(h func(error)) { c.sock.conn.SetErrorHandler(h) }
func (c *SocketClient[T, P, X, P2]) SetMaxMessageSize(s int64) { c.sock.conn.SetMaxMessageSize(s) }
func (c *SocketClient[_, _, _, _]) handleMessage(message []byte, err error) {
if err != nil {
c.log.Error().Err(err).Send()
return
}
if err = c.rpc.handleMessage(message); err != nil { // 2nd handler
c.log.Error().Err(err).Send()
return
}
}
func (c *SocketClient[_, P, X, P2]) Route(in P, out P2) {
rq := P2(new(X))
rq.SetId(in.GetId().String())
rq.SetType(uint8(in.GetType()))
rq.SetPayload(out.GetPayload())
if err := c.rpc.Send(c.sock.conn, rq); err != nil {
c.log.Error().Err(err).Msgf("message route fail")
}
}
// Send makes a blocking call.
func (c *SocketClient[T, P, X, P2]) Send(t T, data any) ([]byte, error) {
c.log.Debug().Str(logger.DirectionField, logger.MarkOut).Msgf("ᵇ%v", t)
rq := P2(new(X))
rq.SetType(uint8(t))
rq.SetPayload(data)
return c.rpc.Call(c.sock.conn, rq)
}
// Notify just sends a message and goes further.
func (c *SocketClient[T, P, X, P2]) Notify(t T, data any) {
c.log.Debug().Str(logger.DirectionField, logger.MarkOut).Msgf("%v", t)
rq := P2(new(X))
rq.SetType(uint8(t))
rq.SetPayload(data)
if err := c.rpc.Send(c.sock.conn, rq); err != nil {
c.log.Error().Err(err).Msgf("notify fail")
}
}
func (c *SocketClient[_, _, _, _]) Disconnect() {
c.sock.conn.Close()
c.rpc.Cleanup()
c.log.Debug().Str(logger.DirectionField, logger.MarkCross).Msg("Close")
}
func (c *SocketClient[_, _, _, _]) Id() Uid { return c.id }
func (c *SocketClient[_, _, _, _]) String() string { return c.Id().String() }

127
pkg/com/map.go Normal file
View file

@ -0,0 +1,127 @@
package com
import (
"fmt"
"iter"
"sync"
)
// Map defines a concurrent-safe map structure.
// Keep in mind that the underlying map structure will grow indefinitely.
type Map[K comparable, V any] struct {
m map[K]V
mu sync.RWMutex
}
func (m *Map[K, _]) Len() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.m)
}
func (m *Map[K, _]) Has(key K) bool {
m.mu.RLock()
_, ok := m.m[key]
m.mu.RUnlock()
return ok
}
// Get returns the value and exists flag (standard map comma-ok idiom).
func (m *Map[K, V]) Get(key K) (V, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
val, ok := m.m[key]
return val, ok
}
func (m *Map[K, V]) Find(key K) V {
v, _ := m.Get(key)
return v
}
func (m *Map[K, V]) String() string {
m.mu.RLock()
defer m.mu.RUnlock()
return fmt.Sprintf("%v", m.m)
}
// FindBy searches for the first value satisfying the predicate.
// Note: This holds a Read Lock during iteration.
func (m *Map[K, V]) FindBy(predicate func(v V) bool) (V, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, v := range m.m {
if predicate(v) {
return v, true
}
}
var zero V
return zero, false
}
// Put sets the value and returns true if the key already existed.
func (m *Map[K, V]) Put(key K, v V) bool {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[K]V)
}
_, exists := m.m[key]
m.m[key] = v
return exists
}
func (m *Map[K, V]) Remove(key K) {
m.mu.Lock()
delete(m.m, key)
m.mu.Unlock()
}
// Pop returns the value and removes it from the map.
// Returns zero value if not found.
func (m *Map[K, V]) Pop(key K) V {
m.mu.Lock()
defer m.mu.Unlock()
val, ok := m.m[key]
if ok {
delete(m.m, key)
}
return val
}
// RemoveL removes the key and returns the new length of the map.
func (m *Map[K, _]) RemoveL(key K) int {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.m, key)
return len(m.m)
}
// Clear empties the map.
func (m *Map[K, V]) Clear() {
m.mu.Lock()
m.m = make(map[K]V)
m.mu.Unlock()
}
// Values returns an iterator for values only.
//
// Usage: for k, v := range m.Values() { ... }
//
// Warning: This holds a Read Lock (RLock) during iteration.
// Do not call Put/Remove on this map inside the loop (Deadlock).
func (m *Map[K, V]) Values() iter.Seq[V] {
return func(yield func(V) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for _, v := range m.m {
if !yield(v) {
return
}
}
}
}

63
pkg/com/map_test.go Normal file
View file

@ -0,0 +1,63 @@
package com
import "testing"
func TestMap_Base(t *testing.T) {
// map map
m := Map[int, int]{m: make(map[int]int)}
if m.Len() > 0 {
t.Errorf("should be empty, %v %v", m.Len(), m.m)
}
k := 0
m.Put(k, 0)
if m.Len() == 0 {
t.Errorf("should not be empty, %v", m.m)
}
if !m.Has(k) {
t.Errorf("should have the key %v, %v", k, m.m)
}
v, ok := m.Get(k)
if v != 0 && !ok {
t.Errorf("should have the key %v and ok, %v %v", k, ok, m.m)
}
_, ok = m.Get(k + 1)
if ok {
t.Errorf("should not find anything, %v %v", ok, m.m)
}
m.Put(1, 1)
v, ok = m.FindBy(func(v int) bool { return v == 1 })
if v != 1 && !ok {
t.Errorf("should have the key %v and ok, %v %v", 1, ok, m.m)
}
sum := 0
for v := range m.Values() {
sum += v
}
if sum != 1 {
t.Errorf("shoud have exact sum of 1, but have %v", sum)
}
m.Remove(1)
if !m.Has(0) || m.Len() > 1 {
t.Errorf("should remove only one element, but has %v", m.m)
}
m.Put(3, 3)
v = m.Pop(3)
if v != 3 {
t.Errorf("should have value %v, but has %v %v", 3, v, m.m)
}
m.Remove(3)
m.Remove(0)
if m.Len() != 0 {
t.Errorf("should be completely empty, but %v", m.m)
}
}
func TestMap_Concurrency(t *testing.T) {
m := Map[int, int]{m: make(map[int]int)}
for i := range 100 {
go m.Put(i, i)
go m.Has(i)
go m.Pop(i)
}
}

178
pkg/com/net.go Normal file
View file

@ -0,0 +1,178 @@
package com
import (
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/giongto35/cloud-game/v3/pkg/network/websocket"
"github.com/goccy/go-json"
"github.com/rs/xid"
)
type Uid struct {
xid.ID
}
var NilUid = Uid{xid.NilID()}
func NewUid() Uid { return Uid{xid.New()} }
func UidFromString(id string) (Uid, error) {
x, err := xid.FromString(id)
if err != nil {
return NilUid, err
}
return Uid{x}, nil
}
func (u Uid) Short() string { return u.String()[:3] + "." + u.String()[len(u.String())-3:] }
type HasCallId interface {
SetGetId(fmt.Stringer)
}
type Writer interface {
Write([]byte)
}
type Packet[T ~uint8] interface {
GetId() Uid
GetType() T
GetPayload() []byte
}
type Packet2[T any] interface {
SetId(string)
SetType(uint8)
SetPayload(any)
SetGetId(fmt.Stringer)
GetPayload() any
*T // non-interface type constraint element
}
type Transport interface {
SetMessageHandler(func([]byte, error))
}
type RPC[T ~uint8, P Packet[T]] struct {
CallTimeout time.Duration
Handler func(P)
Transport Transport
calls Map[Uid, *request]
}
type request struct {
done chan struct{}
err error
response []byte
}
const DefaultCallTimeout = 10 * time.Second
var errCanceled = errors.New("canceled")
var errTimeout = errors.New("timeout")
type (
Client struct {
websocket.Client
}
Server struct {
websocket.Server
}
Connection struct {
conn *websocket.Connection
}
)
func (c *Client) Connect(addr url.URL) (*Connection, error) { return connect(c.Client.Connect(addr)) }
func (s *Server) Origin(host string) { s.Upgrader = websocket.NewUpgrader(host) }
func (s *Server) Connect(w http.ResponseWriter, r *http.Request) (*Connection, error) {
return connect(s.Server.Connect(w, r, nil))
}
func (c *Connection) IsServer() bool { return c.conn.IsServer() }
func (c *Connection) SetMaxReadSize(s int64) { c.conn.SetMaxMessageSize(s) }
func connect(conn *websocket.Connection, err error) (*Connection, error) {
if err != nil {
return nil, err
}
return &Connection{conn: conn}, nil
}
func NewRPC[T ~uint8, P Packet[T]]() *RPC[T, P] {
return &RPC[T, P]{calls: Map[Uid, *request]{m: make(map[Uid]*request, 10)}}
}
func (t *RPC[_, _]) Send(w Writer, packet any) error {
r, err := json.Marshal(packet)
if err != nil {
return err
}
w.Write(r)
return nil
}
func (t *RPC[_, _]) Call(w Writer, rq HasCallId) ([]byte, error) {
id := NewUid()
// set new request id for the external request structure as string
rq.SetGetId(id)
r, err := json.Marshal(rq)
if err != nil {
return nil, err
}
task := &request{done: make(chan struct{})}
t.calls.Put(id, task)
w.Write(r)
select {
case <-task.done:
case <-time.After(t.callTimeout()):
task.err = errTimeout
}
return task.response, task.err
}
func (t *RPC[_, P]) handleMessage(message []byte) error {
res := *new(P)
if err := json.Unmarshal(message, &res); err != nil {
return err
}
// if we have an id, then unblock blocking call with that id
id := res.GetId()
if id != NilUid {
if blocked := t.calls.Pop(id); blocked != nil {
blocked.response = res.GetPayload()
close(blocked.done)
return nil
}
}
if t.Handler != nil {
t.Handler(res)
}
return nil
}
func (t *RPC[_, _]) callTimeout() time.Duration {
if t.CallTimeout > 0 {
return t.CallTimeout
}
return DefaultCallTimeout
}
func (t *RPC[_, _]) Cleanup() {
// drain cancels all what's left in the task queue.
for task := range t.calls.Values() {
if task.err == nil {
task.err = errCanceled
}
close(task.done)
}
}

225
pkg/com/net_test.go Normal file
View file

@ -0,0 +1,225 @@
package com
import (
"encoding/json"
"fmt"
"math/rand/v2"
"net"
"net/http"
"net/url"
"sync"
"testing"
"time"
"github.com/giongto35/cloud-game/v3/pkg/logger"
"github.com/giongto35/cloud-game/v3/pkg/network/websocket"
)
type TestIn struct {
Id Uid
T uint8
Payload json.RawMessage
}
func (i TestIn) GetId() Uid { return i.Id }
func (i TestIn) GetType() uint8 { return i.T }
func (i TestIn) GetPayload() []byte { return i.Payload }
type TestOut struct {
Id string
T uint8
Payload any
}
func (o *TestOut) SetId(s string) { o.Id = s }
func (o *TestOut) SetType(u uint8) { o.T = u }
func (o *TestOut) SetPayload(a any) { o.Payload = a }
func (o *TestOut) SetGetId(stringer fmt.Stringer) { o.Id = stringer.String() }
func (o *TestOut) GetPayload() any { return o.Payload }
func TestWebsocket(t *testing.T) {
testCases := []struct {
name string
test func(t *testing.T)
}{
{"If WebSocket implementation is OK in general", testWebsocket},
}
for _, tc := range testCases {
t.Run(tc.name, tc.test)
}
}
func testWebsocket(t *testing.T) {
port, err := getFreePort()
if err != nil {
t.Logf("couldn't get any free port")
t.Skip()
}
addr := fmt.Sprintf(":%v", port)
server := newServer(addr, t)
client := newClient(t, url.URL{Scheme: "ws", Host: "localhost" + addr, Path: "/ws"})
clDone := client.ProcessPackets(func(in TestIn) error { return nil })
if server.conn == nil {
t.Fatalf("couldn't make new socket")
}
calls := []struct {
packet TestOut
concurrent bool
value any
}{
{packet: TestOut{T: 10, Payload: "test"}, value: "test", concurrent: true},
{packet: TestOut{T: 10, Payload: "test2"}, value: "test2"},
{packet: TestOut{T: 11, Payload: "test3"}, value: "test3"},
{packet: TestOut{T: 99, Payload: ""}, value: ""},
{packet: TestOut{T: 0}},
{packet: TestOut{T: 12, Payload: 123}, value: 123},
{packet: TestOut{T: 10, Payload: false}, value: false},
{packet: TestOut{T: 10, Payload: true}, value: true},
{packet: TestOut{T: 11, Payload: []string{"test", "test", "test"}}, value: []string{"test", "test", "test"}},
{packet: TestOut{T: 22, Payload: []string{}}, value: []string{}},
}
const n = 42
var wait sync.WaitGroup
wait.Add(n * len(calls))
// test
for _, call := range calls {
if call.concurrent {
for range n {
packet := call.packet
go func() {
defer wait.Done()
time.Sleep(time.Duration(rand.IntN(200-100)+100) * time.Millisecond)
vv, err := client.rpc.Call(client.sock.conn, &packet)
err = checkCall(vv, err, call.value)
if err != nil {
t.Errorf("%v", err)
return
}
}()
}
} else {
for range n {
packet := call.packet
vv, err := client.rpc.Call(client.sock.conn, &packet)
err = checkCall(vv, err, call.value)
if err != nil {
wait.Done()
t.Fatalf("%v", err)
} else {
wait.Done()
}
}
}
}
wait.Wait()
client.sock.conn.Close()
client.rpc.Cleanup()
<-clDone
server.conn.Close()
<-server.done
}
func newClient(t *testing.T, addr url.URL) *SocketClient[uint8, TestIn, TestOut, *TestOut] {
connector := Client{}
conn, err := connector.Connect(addr)
if err != nil {
t.Fatalf("error: couldn't connect to %v because of %v", addr.String(), err)
}
rpc := new(RPC[uint8, TestIn])
rpc.calls = Map[Uid, *request]{m: make(map[Uid]*request, 10)}
return &SocketClient[uint8, TestIn, TestOut, *TestOut]{sock: conn, log: logger.Default(), rpc: rpc}
}
func checkCall(v []byte, err error, need any) error {
if err != nil {
return err
}
var value any
if v != nil {
if err = json.Unmarshal(v, &value); err != nil {
return fmt.Errorf("can't unmarshal %v", v)
}
}
nice := true
// cast values after default unmarshal
switch value.(type) {
default:
nice = value == need
case bool:
nice = value == need.(bool)
case float64:
nice = value == float64(need.(int))
case string:
nice = value == need.(string)
case []any:
// let's assume that's strings
vv := value.([]any)
for i := 0; i < len(need.([]string)); i++ {
if vv[i].(string) != need.([]string)[i] {
nice = false
break
}
}
case map[string]any:
// ???
}
if !nice {
return fmt.Errorf("expected %v, but got %v", need, v)
}
return nil
}
type serverHandler struct {
conn *websocket.Connection // ws server reference made dynamically on HTTP request
done chan struct{}
}
func (s *serverHandler) serve(t *testing.T) func(w http.ResponseWriter, r *http.Request) {
connector := Server{}
return func(w http.ResponseWriter, r *http.Request) {
sock, err := connector.Server.Connect(w, r, nil)
if err != nil {
t.Fatalf("couldn't init socket server")
}
s.conn = sock
s.conn.SetMessageHandler(func(m []byte, err error) { s.conn.Write(m) }) // echo
s.done = s.conn.Listen()
}
}
func newServer(addr string, t *testing.T) *serverHandler {
var wg sync.WaitGroup
handler := serverHandler{}
http.HandleFunc("/ws", handler.serve(t))
wg.Add(1)
go func() {
wg.Done()
if err := http.ListenAndServe(addr, nil); err != nil {
t.Errorf("no server, %v", err)
return
}
}()
wg.Wait()
return &handler
}
func getFreePort() (port int, err error) {
var a *net.TCPAddr
var l *net.TCPListener
if a, err = net.ResolveTCPAddr("tcp", ":0"); err == nil {
if l, err = net.ListenTCP("tcp", a); err == nil {
defer func() { _ = l.Close() }()
return l.Addr().(*net.TCPAddr).Port, nil
}
}
return
}

446
pkg/config/config.yaml Normal file
View file

@ -0,0 +1,446 @@
# The main config file
# Note.
# Be aware that when this configuration is being overwritten
# by another configuration, any empty nested part
# in the further configurations will reset (empty out) all the values.
# For example:
# the main config second config result
# ... ... ...
# list: list: list:
# gba: gba: gba:
# lib: mgba_libretro lib: ""
# roms: [ "gba", "gbc" ] roms: []
# ... ...
#
# So do not leave empty nested keys.
# for the compatibility purposes
version: 3
# new decentralized library of games
library:
# optional alias file for overriding game names from the basePath path
aliasFile: alias.txt
# root folder for the library (where games are stored)
basePath: assets/games
# a list of ignored words in the ROM filenames
ignored:
- neogeo
- pgm
# DOSBox filesystem state
- .pure
# an explicit list of supported file extensions
# which overrides Libretro emulator ROMs configs
supported:
# print some additional info
verbose: true
# enable library directory live reload
# (experimental)
watchMode: false
coordinator:
# debugging switch
# - shows debug logs
# - allows selecting worker instances
debug: false
# selects free workers:
# - empty value (default, any free)
# - ping (with the lowest ping)
selector:
monitoring:
port: 6601
# enable Go profiler HTTP server
profilingEnabled: false
metricEnabled: false
urlPrefix: /coordinator
# a custom Origins for incoming Websocket connections:
# "" -- checks same origin policy
# "*" -- allows all
# "your address" -- checks for that address
origin:
userWs:
workerWs:
# max websocket message size in bytes
maxWsSize: 32000000
# HTTP(S) server config
server:
address: :8000
cacheControl: "max-age=259200, must-revalidate"
frameOptions: ""
https: false
# Letsencrypt or self cert config
tls:
address: :443
# allowed host name
domain:
# if both are set then will use certs
# and Letsencryt instead
httpsCert:
httpsKey:
analytics:
inject: false
gtag:
worker:
# show more logs
debug: false
library:
# root folder for the library (where games are stored)
basePath: assets/games
network:
# a coordinator address to connect to
coordinatorAddress: localhost:8000
# where to connect
endpoint: /wso
# ping endpoint
pingEndpoint: /echo
# set public ping address (IP or hostname)
publicAddress:
# make coordinator connection secure (wss)
secure: false
# ISO Alpha-2 country code to group workers by zones
zone:
monitoring:
# monitoring server port
port: 6602
profilingEnabled: false
# monitoring server URL prefix
metricEnabled: false
urlPrefix: /worker
server:
address: :9000
https: false
tls:
address: :444
# LetsEncrypt config
# allowed host name
domain:
# Own certs config
httpsCert:
httpsKey:
# optional server tag
tag:
emulator:
# set the total number of threads for the image processing
# (removed)
threads: 0
# enable autosave for emulator states if set to a non-zero value of seconds
autosaveSec: 0
# save directory for emulator states
# special tag {user} will be replaced with current user's home dir
storage: "{user}/.cr/save"
# path for storing emulator generated files
localPath: "./libretro"
# checks if the system supports running an emulator at startup
failFast: true
# do not send late video frames
skipLateFrames: false
# log dropped frames (temp)
logDroppedFrames: false
libretro:
# use zip compression for emulator save states
saveCompression: true
# Sets a limiter function for some spammy core callbacks.
# 0 - disabled, otherwise -- time in milliseconds for ignoring repeated calls except the last.
debounceMs: 0
# Allow duplicate frames
dup: true
# Libretro cores logging level: DEBUG = 0, INFO, WARN, ERROR, DUMMY = INT_MAX
logLevel: 1
cores:
paths:
libs: 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
# external cross-process mutex lock
extLock: "{user}/.cr/cloud-game.lock"
map:
darwin:
amd64:
arch: x86_64
ext: .dylib
os: osx
vendor: apple
arm64:
arch: arm64
ext: .dylib
os: osx
vendor: apple
linux:
amd64:
arch: x86_64
ext: .so
os: linux
arm:
arch: armv7-neon-hf
ext: .so
os: linux
windows:
amd64:
arch: x86_64
ext: .dll
os: windows
main:
type: buildbot
url: https://buildbot.libretro.com/nightly
# if repo has file compression
compression: zip
# a secondary repo to use i.e. for not found in the main cores
secondary:
type: github
url: https://github.com/sergystepanov/libretro-spiegel/raw/main
compression: zip
# Libretro core configuration
#
# The emulator selection will happen in this order:
# - based on the folder name in the folder param
# - based on the folder name (core name) in the list (i.e. nes, snes)
# - based on the rom names in the roms param
#
# Available config params:
# - altRepo (bool) prioritize secondary repo as the download source
# - lib (string)
# - roms ([]string)
# - scale (int) scales the output video frames by this factor.
# - folder (string)
# By default emulator selection is based on the folder named as cores
# in the list (i.e. nes, snes) but if you specify folder param,
# then it will try to load the ROM file from that folder first.
# - width (int) -- broken
# - height (int) -- broken
# - ratio (float)
# - isGlAllowed (bool)
# - usesLibCo (bool)
# - hasMultitap (bool) -- (removed)
# - coreAspectRatio (bool) -- (deprecated) correct the aspect ratio on the client with the info from the core.
# - hid (map[int][]int)
# A list of device IDs to bind to the input ports.
# Can be seen in human readable form in the console when worker.debug is enabled.
# Some cores allow binding multiple devices to a single port (DosBox), but typically,
# you should bind just one device to one port.
# - kbMouseSupport (bool) -- (temp) a flag if the core needs the keyboard and mouse on the client
# - nonBlockingSave (bool) -- write save file in a non-blocking way, needed for huge save files
# - vfr (bool)
# (experimental)
# Enable variable frame rate only for cores that can't produce a constant frame rate.
# By default, we assume that cores output frames at a constant rate which equals
# their tick rate (1/system FPS), but OpenGL cores like N64 may have significant
# frame rendering time inconsistencies. In general, VFR for CFR cores leads to
# noticeable video stutter (with the current frame rendering time calculations).
# - options ([]string) a list of Libretro core options for tweaking.
# All keys of the options should be in the double quotes in order to preserve upper-case symbols.
# - options4rom (rom[[]string])
# A list of core options to override for a specific core depending on the current ROM name.
# - hacks ([]string) a list of hacks.
# Available:
# - skip_hw_context_destroy -- don't destroy OpenGL context during Libretro core deinit.
# May help with crashes, for example, with PPSSPP.
# - skip_same_thread_save -- skip thread lock save (used with PPSSPP).
# - uniqueSaveDir (bool) -- needed only for cores (like DosBox) that persist their state into one shared file.
# This will allow for concurrent reading and saving of current states.
# - saveStateFs (string) -- the name of the file that will be initially copied into the save folder.
# All * symbols will be replaced to the name of the ROM.
list:
gba:
lib: mgba_libretro
roms: [ "gba", "gbc" ]
options:
mgba_audio_low_pass_filter: disabled
mgba_audio_low_pass_range: 50
pcsx:
lib: pcsx_rearmed_libretro
roms: [ "cue", "chd" ]
# example of folder override
folder: psx
# see: https://github.com/libretro/pcsx_rearmed/blob/master/frontend/libretro_core_options.h
options:
"pcsx_rearmed_show_bios_bootlogo": enabled
"pcsx_rearmed_drc": enabled
"pcsx_rearmed_display_internal_fps": disabled
# MAME core requires additional manual setup, please read:
# https://docs.libretro.com/library/fbneo/
mame:
lib: fbneo_libretro
folder: mame
roms: [ "zip" ]
nes:
lib: nestopia_libretro
roms: [ "nes" ]
options:
nestopia_aspect: "uncorrected"
snes:
lib: snes9x_libretro
roms: [ "smc", "sfc", "swc", "fig", "bs" ]
hid:
# set the 2nd port to RETRO_DEVICE_JOYPAD_MULTITAP ((1<<8) | 1) as SNES9x requires it
# in order to support up to 5-player games
# see: https://nintendo.fandom.com/wiki/Super_Multitap
1: 257
n64:
lib: mupen64plus_next_libretro
roms: [ "n64", "v64", "z64" ]
isGlAllowed: true
usesLibCo: true
vfr: true
# see: https://github.com/libretro/mupen64plus-libretro-nx/blob/master/libretro/libretro_core_options.h
options:
"mupen64plus-169screensize": 640x360
"mupen64plus-43screensize": 320x240
"mupen64plus-EnableCopyColorToRDRAM": Off
"mupen64plus-EnableCopyDepthToRDRAM": Off
"mupen64plus-EnableEnhancedTextureStorage": True
"mupen64plus-EnableFBEmulation": True
"mupen64plus-EnableLegacyBlending": True
"mupen64plus-FrameDuping": True
"mupen64plus-MaxTxCacheSize": 8000
"mupen64plus-ThreadedRenderer": False
"mupen64plus-cpucore": dynamic_recompiler
"mupen64plus-pak1": memory
"mupen64plus-rdp-plugin": gliden64
"mupen64plus-rsp-plugin": hle
"mupen64plus-astick-sensitivity": 100
dos:
lib: dosbox_pure_libretro
roms: [ "zip", "cue" ]
folder: dos
kbMouseSupport: true
nonBlockingSave: true
saveStateFs: "*.pure.zip"
hid:
0: [ 257, 513 ]
1: [ 257, 513 ]
2: [ 257, 513 ]
3: [ 257, 513 ]
options:
"dosbox_pure_conf": "outside"
"dosbox_pure_force60fps": "true"
encoder:
audio:
# audio frame duration needed for WebRTC (Opus)
# most of the emulators have ~1400 samples per a video frame,
# so we keep the frame buffer roughly half of that size or 2 RTC packets per frame
# (deprecated) due to frames
frame: 10
# dynamic frames for Opus encoder
frames:
- 10
- 5
# speex (2), linear (1) or nearest neighbour (0) audio resampler
# linear should sound slightly better than 0
resampler: 2
video:
# h264, vpx (vp8) or vp9
codec: h264
# Threaded encoder if supported, 0 - auto, 1 - nope, >1 - multi-threaded
threads: 0
# see: https://trac.ffmpeg.org/wiki/Encode/H.264
h264:
# crf, cbr
mode: crf
# Constant Rate Factor (CRF) 0-51 (default: 23)
crf: 23
# Rate control options
# set the maximum bitrate
maxRate: 0
# set the expected client buffer size
bufSize: 0
# ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow, placebo
preset: superfast
# baseline, main, high, high10, high422, high444
profile: baseline
# film, animation, grain, stillimage, psnr, ssim, fastdecode, zerolatency
tune: zerolatency
# 0-3
logLevel: 0
# see: https://www.webmproject.org/docs/encoder-parameters
vpx:
# target bitrate (KBit/s)
bitrate: 1200
# force keyframe interval
keyframeInterval: 5
# game recording
# (experimental)
# recording allows export RAW a/v streams of games
# by default, it will export audio as WAV files,
# video as a list of PNG-encoded images, and
# one additional FFMPEG concat demux file
recording:
enabled: false
# name contains the name of the recording dir (or zip)
# format:
# %date:go_time_format% -- refer: https://go.dev/src/time/format.go
# %user% -- user name who started the recording
# %game% -- game name (game ROM name)
# %rand:len% -- a random string of given length
# as example: 20210101101010_yeE_user1_badApple
name: "%date:20060102150405%_%rand:3%_%user%_%game%"
# zip and remove recording dir on completion
zip: true
# save directory
folder: ./recording
# cloud storage options
# it is mandatory to use a cloud storage when running
# a distributed multi-server configuration in order to
# share save states between nodes (resume games on a different worker)
storage:
# cloud storage provider:
# - empty (No op storage stub)
# - s3 (S3 API compatible object storage)
provider:
s3Endpoint:
s3BucketName:
s3AccessKeyId:
s3SecretAccessKey:
webrtc:
# turn off default Pion interceptors (see: https://github.com/pion/interceptor)
# (performance)
disableDefaultInterceptors: false
# indicates the role of the DTLS transport (see: https://github.com/pion/webrtc/blob/master/dtlsrole.go)
# (debug)
# - (default)
# - 1 (auto)
# - 2 (client)
# - 3 (server)
dtlsRole:
# a list of STUN/TURN servers to use
iceServers:
- urls: stun:stun.l.google.com:19302
# configures whether the ice agent should be a lite agent (true/false)
# (performance)
# don't use iceServers when enabled
iceLite: false
# ICE configuration
# by default, ICE ports are random and unlimited
# alternatives:
# 1. instead of random unlimited port range for
# WebRTC connections, these params limit port range of ICE connections
icePorts:
min:
max:
# 2. select a single port to forward all ICE connections there
singlePort:
# override ICE candidate IP, see: https://github.com/pion/webrtc/issues/835,
# can be used for Docker bridged network internal IP override
iceIpMap:
# set additional log level for WebRTC separately
# -1 - trace, 6 - nothing, ..., debug - 0
logLevel: 6

52
pkg/config/coordinator.go Normal file
View file

@ -0,0 +1,52 @@
package config
import "flag"
type CoordinatorConfig struct {
Coordinator Coordinator
Emulator Emulator
Library Library
Recording Recording
Version Version
Webrtc Webrtc
}
type Coordinator struct {
Analytics Analytics
Debug bool
Library Library
MaxWsSize int64
Monitoring Monitoring
Origin struct {
UserWs string
WorkerWs string
}
Selector string
Server Server
}
// Analytics is optional Google Analytics
type Analytics struct {
Inject bool
Gtag string
}
const SelectByPing = "ping"
// allows custom config path
var coordinatorConfigPath string
func NewCoordinatorConfig() (conf CoordinatorConfig, paths []string) {
paths, err := LoadConfig(&conf, coordinatorConfigPath)
if err != nil {
panic(err)
}
return
}
func (c *CoordinatorConfig) ParseFlags() {
c.Coordinator.Server.WithFlags()
flag.IntVar(&c.Coordinator.Monitoring.Port, "monitoring.port", c.Coordinator.Monitoring.Port, "Monitoring server port")
flag.StringVar(&coordinatorConfigPath, "c-conf", coordinatorConfigPath, "Set custom configuration file path")
flag.Parse()
}

View file

@ -1,49 +0,0 @@
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/monitoring"
"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"
flag "github.com/spf13/pflag"
)
type Config struct {
Coordinator struct {
DebugHost string
Library games.Config
Monitoring monitoring.Config
Server shared.Server
Analytics Analytics
}
Emulator emulator.Emulator
Environment shared.Environment
Webrtc webrtcConfig.Webrtc
}
// Analytics is optional Google Analytics
type Analytics struct {
Inject bool
Gtag string
}
// allows custom config path
var configPath string
func NewConfig() (conf Config) {
err := config.LoadConfig(&conf, configPath)
if err != nil {
panic(err)
}
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()
}

154
pkg/config/emulator.go Normal file
View file

@ -0,0 +1,154 @@
package config
import (
"errors"
"path"
"path/filepath"
"runtime"
"strings"
)
type Emulator struct {
FailFast bool
Threads int
Storage string
LocalPath string
Libretro LibretroConfig
AutosaveSec int
SkipLateFrames bool
LogDroppedFrames bool
}
type LibretroConfig struct {
Cores struct {
Paths struct {
Libs string
}
Repo LibretroRemoteRepo
List map[string]LibretroCoreConfig
}
DebounceMs int
Dup bool
SaveCompression bool
LogLevel int
}
type LibretroRemoteRepo struct {
Sync bool
ExtLock string
Map map[string]map[string]LibretroRepoMapInfo
Main LibretroRepoConfig
Secondary LibretroRepoConfig
}
// LibretroRepoMapInfo contains Libretro core lib platform info.
// And the cores are just C-compiled libraries.
// See: https://buildbot.libretro.com/nightly.
type LibretroRepoMapInfo struct {
Arch string // bottom: x86_64, x86, ...
Ext string // platform dependent library file extension (dot-prefixed)
Os string // middle: windows, ios, ...
Vendor string // top level: apple, nintendo, ...
}
type LibretroRepoConfig struct {
Type string
Url string
Compression string
}
// Guess tries to map OS + CPU architecture to the corresponding remote URL path.
// See: https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63.
func (lrp LibretroRemoteRepo) Guess() (LibretroRepoMapInfo, error) {
if os, ok := lrp.Map[runtime.GOOS]; ok {
if arch, ok2 := os[runtime.GOARCH]; ok2 {
return arch, nil
}
}
return LibretroRepoMapInfo{},
errors.New("core mapping not found for " + runtime.GOOS + ":" + runtime.GOARCH)
}
type LibretroCoreConfig struct {
AltRepo bool
AutoGlContext bool // hack: keep it here to pass it down the emulator
CoreAspectRatio bool
Folder string
Hacks []string
Height int
Hid map[int][]int
IsGlAllowed bool
KbMouseSupport bool
Lib string
NonBlockingSave bool
Options map[string]string
Options4rom map[string]map[string]string // <(^_^)>
Roms []string
SaveStateFs string
Scale float64
UniqueSaveDir bool
UsesLibCo bool
VFR bool
Width int
}
type CoreInfo struct {
Id string
Name string
AltRepo 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)
return conf
}
// GetEmulator tries to find a suitable emulator.
// !to remove quadratic complexity
func (e Emulator) GetEmulator(rom string, path string) string {
found := ""
for emu, core := range e.Libretro.Cores.List {
for _, romName := range core.Roms {
if rom == romName {
found = emu
if p := strings.SplitN(filepath.ToSlash(path), "/", 2); len(p) > 1 {
folder := p[0]
if (folder != "" && folder == core.Folder) || folder == emu {
return emu
}
}
}
}
}
return found
}
func (e Emulator) GetSupportedExtensions() []string {
var extensions []string
for _, core := range e.Libretro.Cores.List {
extensions = append(extensions, core.Roms...)
}
return extensions
}
func (e Emulator) SessionStoragePath() string {
return e.Storage
}
func (l *LibretroConfig) GetCores() (cores []CoreInfo) {
for k, core := range l.Cores.List {
cores = append(cores, CoreInfo{Id: k, Name: core.Lib, AltRepo: core.AltRepo})
}
return
}
func (l *LibretroConfig) GetCoresStorePath() string {
pth, err := filepath.Abs(l.Cores.Paths.Libs)
if err != nil {
return ""
}
return pth
}

View file

@ -1,111 +0,0 @@
package emulator
import (
"path"
"path/filepath"
"strings"
)
type Emulator struct {
Scale int
AspectRatio struct {
Keep bool
Width int
Height int
}
Storage string
Libretro LibretroConfig
}
type LibretroConfig struct {
Cores struct {
Paths struct {
Libs string
Configs string
}
Repo struct {
Sync bool
ExtLock string
Main LibretroRepoConfig
Secondary LibretroRepoConfig
}
List map[string]LibretroCoreConfig
}
}
type LibretroRepoConfig struct {
Type string
Url string
Compression string
}
type LibretroCoreConfig struct {
Lib string
Config string
Roms []string
Folder 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
}
// GetEmulator tries to find a suitable emulator.
// !to remove quadratic complexity
func (e Emulator) GetEmulator(rom string, path string) string {
found := ""
for emu, core := range e.Libretro.Cores.List {
for _, romName := range core.Roms {
if rom == romName {
found = emu
if p := strings.SplitN(filepath.ToSlash(path), "/", 2); len(p) > 1 {
folder := p[0]
if (folder != "" && folder == core.Folder) || folder == emu {
return emu
}
}
}
}
}
return found
}
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
}

View file

@ -1,4 +1,4 @@
package emulator
package config
import "testing"
@ -29,20 +29,17 @@ func TestGetEmulator(t *testing.T) {
},
{
rom: "nes",
path: "test/game.nes",
path: "test2/game.nes",
config: map[string]LibretroCoreConfig{
"snes": {Roms: []string{"nes"}},
"snes": {Roms: []string{"snes"}},
"nes": {Roms: []string{"nes"}},
},
emulator: "nes",
},
}
emu := Emulator{
Libretro: LibretroConfig{},
}
for _, test := range tests {
emu := Emulator{Libretro: LibretroConfig{}}
emu.Libretro.Cores.List = test.config
em := emu.GetEmulator(test.rom, test.path)
if test.emulator != em {

View file

@ -1,32 +0,0 @@
package encoder
type Encoder struct {
Audio Audio
Video Video
WithoutGame bool
}
type Audio struct {
Channels int
Frame int
Frequency int
}
type Video struct {
Codec string
H264 struct {
Crf uint8
Preset string
Profile string
Tune string
LogLevel int
}
Vpx struct {
Bitrate uint
KeyframeInterval uint
}
}
func (a *Audio) GetFrameDuration() int {
return a.Frequency * a.Frame / 1000 * a.Channels
}

View file

@ -1,26 +1,163 @@
package config
import (
"bytes"
"embed"
"os"
"path/filepath"
"strings"
"github.com/kkyr/fig"
"github.com/knadh/koanf/maps"
"github.com/knadh/koanf/v2"
"gopkg.in/yaml.v3"
)
const EnvPrefix = "CLOUD_GAME_"
var (
//go:embed config.yaml
conf embed.FS
)
type Kv = map[string]any
type Bytes []byte
func (b *Bytes) ReadBytes() ([]byte, error) { return *b, nil }
func (b *Bytes) Read() (Kv, error) { return nil, nil }
type File string
func (f *File) ReadBytes() ([]byte, error) { return os.ReadFile(string(*f)) }
func (f *File) Read() (Kv, error) { return nil, nil }
type YAML struct{}
func (p *YAML) Marshal(Kv) ([]byte, error) { return nil, nil }
func (p *YAML) Unmarshal(b []byte) (Kv, error) {
var out Kv
klw := keysToLower(b)
decoder := yaml.NewDecoder(bytes.NewReader(klw))
if err := decoder.Decode(&out); err != nil {
return nil, err
}
return out, nil
}
// keysToLower iterates YAML bytes and tries to lower the keys.
// Used for merging with environment vars which are lowered as well.
func keysToLower(in []byte) []byte {
l, r, ignore := 0, 0, false
for i, b := range in {
switch b {
case '#': // skip comments
ignore = true
case ':': // lower left chunk before the next : symbol
if ignore {
continue
}
r = i
ignore = true
for j := l; j <= r; j++ {
c := in[j]
// we skip the line with the first explicit " string symbol
if c == '"' {
break
}
if 'A' <= c && c <= 'Z' {
in[j] += 'a' - 'A'
}
}
case '\n':
l = i
ignore = false
}
}
return in
}
type Env string
func (e *Env) ReadBytes() ([]byte, error) { return nil, nil }
func (e *Env) Read() (Kv, error) {
var keys []string
for _, k := range os.Environ() {
if strings.HasPrefix(k, string(*e)) {
keys = append(keys, k)
}
}
mp := make(Kv)
for _, k := range keys {
parts := strings.SplitN(k, "=", 2)
if parts == nil {
continue
}
n := strings.ToLower(strings.TrimPrefix(parts[0], string(*e)))
if n == "" {
continue
}
// convert VAR_VAR to VAR.VAR or if we need to preserve _
// i.e. VAR_VAR__KEY_HAS_SLASHES to VAR.VAR.KEY_HAS_SLASHES
// with the result: VAR: { VAR: { KEY_HAS_SLASHES: '' } } }
x := strings.Index(n, "__")
var key string
if x == -1 {
key = strings.Replace(n, "_", ".", -1)
} else {
key = strings.Replace(n[:x+1], "_", ".", -1) + n[x+2:]
}
if len(parts) > 1 {
mp[key] = parts[1]
}
}
return maps.Unflatten(mp, "."), nil
}
// 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) error {
envPrefix := "CLOUD_GAME"
dirs := []string{path}
if path == "" {
dirs = append(dirs, ".", "configs", "../../../configs")
if home, err := os.UserHomeDir(); err == nil {
dirs = append(dirs, home+"/.cr")
func LoadConfig(config any, path string) (loaded []string, err error) {
dirs := []string{".", "configs", "../../../configs"}
if path != "" {
dirs = append([]string{path}, dirs...)
}
homeDir := ""
if home, err := os.UserHomeDir(); err == nil {
homeDir = home + "/.cr"
dirs = append(dirs, homeDir)
}
k := koanf.New("_") // move to global scope if configs become dynamic
defer k.Delete("")
data, err := conf.ReadFile("config.yaml")
if err != nil {
return nil, err
}
conf := Bytes(data)
if err := k.Load(&conf, &YAML{}); err != nil {
return nil, err
}
loaded = append(loaded, "default")
for _, dir := range dirs {
path := filepath.Join(filepath.Clean(dir), "config.yaml")
f := File(path)
if _, err := os.Stat(string(f)); !os.IsNotExist(err) {
if err := k.Load(&f, &YAML{}); err != nil {
return loaded, err
}
loaded = append(loaded, path)
}
}
if err := fig.Load(config, fig.Dirs(dirs...), fig.UseEnv(envPrefix)); err != nil {
return err
env := Env(EnvPrefix)
if err := k.Load(&env, nil); err != nil {
return loaded, err
}
return nil
if err := k.Unmarshal("", config); err != nil {
return loaded, err
}
return loaded, nil
}

63
pkg/config/loader_test.go Normal file
View file

@ -0,0 +1,63 @@
package config
import (
"os"
"reflect"
"testing"
)
func TestConfigEnv(t *testing.T) {
var out WorkerConfig
_ = os.Setenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[0]", "10")
_ = os.Setenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[1]", "5")
defer func() { _ = os.Unsetenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[0]") }()
defer func() { _ = os.Unsetenv("CLOUD_GAME_ENCODER_AUDIO_FRAMES[1]") }()
_ = os.Setenv("CLOUD_GAME_EMULATOR_LIBRETRO_CORES_LIST_PCSX_OPTIONS__PCSX_REARMED_DRC", "x")
defer func() {
_ = os.Unsetenv("CLOUD_GAME_EMULATOR_LIBRETRO_CORES_LIST_PCSX_OPTIONS__PCSX_REARMED_DRC")
}()
_, err := LoadConfig(&out, "")
if err != nil {
t.Fatal(err)
}
for i, x := range []float32{10, 5} {
if out.Encoder.Audio.Frames[i] != x {
t.Errorf("%v is not [10, 5]", out.Encoder.Audio.Frames)
t.Failed()
}
}
v := out.Emulator.Libretro.Cores.List["pcsx"].Options["pcsx_rearmed_drc"]
if v != "x" {
t.Errorf("%v is not x", v)
}
}
func Test_keysToLower(t *testing.T) {
type args struct {
in []byte
}
tests := []struct {
name string
args args
want []byte
}{
{name: "empty", args: args{in: []byte{}}, want: []byte{}},
{name: "case", args: args{
in: []byte("KEY:1\n#Comment with:\n KeY123_NamE: 1\n\n\n\nAAA:123\n \"KeyKey\":2\n"),
},
want: []byte("key:1\n#Comment with:\n key123_name: 1\n\n\n\naaa:123\n \"KeyKey\":2\n"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := keysToLower(tt.args.in); !reflect.DeepEqual(got, tt.want) {
t.Errorf("keysToLower() = %v, want %v", string(got), string(tt.want))
}
})
}
}

View file

@ -1,10 +0,0 @@
package monitoring
type Config struct {
Port int
URLPrefix string
MetricEnabled bool `json:"metric_enabled"`
ProfilingEnabled bool `json:"profiling_enabled"`
}
func (c *Config) IsEnabled() bool { return c.MetricEnabled || c.ProfilingEnabled }

66
pkg/config/shared.go Normal file
View file

@ -0,0 +1,66 @@
package config
import "flag"
type Version int
type Library struct {
// filename of the alias' file
AliasFile string
// some directory which is going to be
// the root folder for the library
BasePath string
// a list of supported file extensions
Supported []string
// a list of ignored words in the files
Ignored []string
// print some additional info
Verbose bool
// enable directory changes watch
WatchMode bool
}
func (l Library) GetSupportedExtensions() []string { return l.Supported }
type Monitoring struct {
Port int
URLPrefix string
MetricEnabled bool `json:"metric_enabled"`
ProfilingEnabled bool `json:"profiling_enabled"`
}
func (c *Monitoring) IsEnabled() bool { return c.MetricEnabled || c.ProfilingEnabled }
type Server struct {
Address string
CacheControl string
FrameOptions string
Https bool
Tls struct {
Address string
Domain string
HttpsKey string
HttpsCert string
}
}
type Recording struct {
Enabled bool
Name string
Folder string
Zip bool
}
func (s *Server) WithFlags() {
flag.StringVar(&s.Address, "address", s.Address, "HTTP server address (host:port)")
flag.StringVar(&s.Tls.Address, "httpsAddress", s.Tls.Address, "HTTPS server address (host:port)")
flag.StringVar(&s.Tls.HttpsKey, "httpsKey", s.Tls.HttpsKey, "HTTPS key")
flag.StringVar(&s.Tls.HttpsCert, "httpsCert", s.Tls.HttpsCert, "HTTPS chain")
}
func (s *Server) GetAddr() string {
if s.Https {
return s.Tls.Address
}
return s.Address
}

View file

@ -1,41 +0,0 @@
package shared
import (
"github.com/giongto35/cloud-game/v2/pkg/environment"
flag "github.com/spf13/pflag"
)
type Environment environment.Env
type Server struct {
Address string
Https bool
Tls struct {
Address string
Domain string
HttpsKey string
HttpsCert string
}
}
func (s *Server) WithFlags() {
flag.StringVar(&s.Address, "address", s.Address, "HTTP server address (host:port)")
flag.StringVar(&s.Tls.Address, "httpsAddress", s.Tls.Address, "HTTPS server address (host:port)")
flag.StringVar(&s.Tls.HttpsKey, "httpsKey", s.Tls.HttpsKey, "HTTPS key")
flag.StringVar(&s.Tls.HttpsCert, "httpsCert", s.Tls.HttpsCert, "HTTPS chain")
}
func (s *Server) GetAddr() string {
if s.Https {
return s.Tls.Address
}
return s.Address
}
func (env *Environment) Get() environment.Env {
return (environment.Env)(*env)
}
func (env *Environment) WithFlags() {
flag.StringVar((*string)(env), "env", string(*env), "Specify environment type: [dev, staging, prod]")
}

26
pkg/config/webrtc.go Normal file
View file

@ -0,0 +1,26 @@
package config
type Webrtc struct {
DisableDefaultInterceptors bool
DtlsRole byte
IceServers []IceServer
IcePorts struct {
Min uint16
Max uint16
}
IceIpMap string
IceLite bool
SinglePort int
LogLevel int
}
type IceServer struct {
Urls string `json:"urls,omitempty"`
Username string `json:"username,omitempty"`
Credential string `json:"credential,omitempty"`
}
func (w *Webrtc) HasDtlsRole() bool { return w.DtlsRole > 0 }
func (w *Webrtc) HasPortRange() bool { return w.IcePorts.Min > 0 && w.IcePorts.Max > 0 }
func (w *Webrtc) HasSinglePort() bool { return w.SinglePort > 0 }
func (w *Webrtc) HasIceIpMap() bool { return w.IceIpMap != "" }

View file

@ -1,24 +0,0 @@
package webrtc
import "github.com/giongto35/cloud-game/v2/pkg/config/encoder"
type Webrtc struct {
DisableDefaultInterceptors bool
IceServers []IceServer
IcePorts struct {
Min uint16
Max uint16
}
IceIpMap string
}
type IceServer struct {
Url string
Username string
Credential string
}
type Config struct {
Encoder encoder.Encoder
Webrtc Webrtc
}

View file

@ -1,31 +1,38 @@
package worker
package config
import (
"log"
"flag"
"fmt"
"net"
"net/url"
"path/filepath"
"strings"
"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/monitoring"
"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/environment"
flag "github.com/spf13/pflag"
"github.com/giongto35/cloud-game/v3/pkg/os"
)
type Config struct {
Encoder encoder.Encoder
Emulator emulator.Emulator
Environment shared.Environment
Worker Worker
Webrtc webrtcConfig.Webrtc
type WorkerConfig struct {
Encoder Encoder
Emulator Emulator
Library Library
Recording Recording
Storage Storage
Worker Worker
Webrtc Webrtc
Version Version
}
type Storage struct {
Provider string
S3Endpoint string
S3BucketName string
S3AccessKeyId string
S3SecretAccessKey string
}
type Worker struct {
Monitoring monitoring.Config
Debug bool
Monitoring Monitoring
Network struct {
CoordinatorAddress string
Endpoint string
@ -34,43 +41,85 @@ type Worker struct {
Secure bool
Zone string
}
Server shared.Server
Server Server
Tag string
}
type Encoder struct {
Audio Audio
Video Video
}
type Audio struct {
Frames []float32
Resampler int
}
type Video struct {
Codec string
Threads int
H264 struct {
Mode string
Crf uint8
MaxRate int
BufSize int
LogLevel int32
Preset string
Profile string
Tune string
}
Vpx struct {
Bitrate uint
KeyframeInterval uint
}
}
// allows custom config path
var configPath string
var workerConfigPath string
func NewConfig() (conf Config) {
_ = config.LoadConfig(&conf, configPath)
func NewWorkerConfig() (conf WorkerConfig, paths []string) {
paths, err := LoadConfig(&conf, workerConfigPath)
if err != nil {
panic(err)
}
conf.expandSpecialTags()
conf.fixValues()
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()
func (c *WorkerConfig) ParseFlags() {
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.StringVar(&c.Worker.Network.Zone, "zone", c.Worker.Network.Zone, "Worker network zone (us, eu, etc.)")
flag.StringVarP(&configPath, "conf", "c", configPath, "Set custom configuration file path")
flag.StringVar(&workerConfigPath, "w-conf", workerConfigPath, "Set custom configuration file path")
flag.Parse()
}
// expandSpecialTags replaces all the special tags in the config.
func (c *Config) expandSpecialTags() {
func (c *WorkerConfig) expandSpecialTags() {
tag := "{user}"
for _, dir := range []*string{&c.Emulator.Storage, &c.Emulator.Libretro.Cores.Repo.ExtLock} {
if *dir == "" || !strings.Contains(*dir, tag) {
continue
}
userHomeDir, err := environment.GetUserHome()
userHomeDir, err := os.GetUserHome()
if err != nil {
log.Fatalln("couldn't read user home directory", err)
panic(fmt.Sprintf("couldn't read user home directory, %v", err))
}
*dir = strings.Replace(*dir, tag, userHomeDir, -1)
*dir = filepath.FromSlash(*dir)
}
}
// fixValues tries to fix some values otherwise hard to set externally.
func (c *WorkerConfig) fixValues() {
// with ICE lite we clear ICE servers
if c.Webrtc.IceLite {
c.Webrtc.IceServers = []IceServer{}
}
}
@ -78,7 +127,7 @@ func (c *Config) expandSpecialTags() {
func (w *Worker) GetAddr() string { return w.Server.GetAddr() }
// GetPingAddr returns exposed to clients server ping endpoint address.
func (w *Worker) GetPingAddr(address string) string {
func (w *Worker) GetPingAddr(address string) url.URL {
_, srcPort, _ := net.SplitHostPort(w.GetAddr())
dstHost, _, _ := net.SplitHostPort(address)
address = net.JoinHostPort(dstHost, srcPort)
@ -98,5 +147,10 @@ func (w *Worker) GetPingAddr(address string) string {
if w.Server.Https {
pingURL.Scheme = "https"
}
return pingURL.String()
return pingURL
}
func (w *Worker) GetPort(address string) string {
_, port, _ := net.SplitHostPort(address)
return port
}

View file

@ -1,34 +0,0 @@
package coordinator
import (
"fmt"
"log"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/gorilla/websocket"
)
type BrowserClient struct {
*cws.Client
SessionID string
RoomID string
WorkerID string // TODO: how about pointer to workerClient?
}
// NewCoordinatorClient returns a client connecting to browser.
// This connection exchanges information between browser and coordinator.
func NewBrowserClient(c *websocket.Conn, browserID string) *BrowserClient {
return &BrowserClient{
Client: cws.NewClient(c),
SessionID: browserID,
}
}
// Register new log
func (bc *BrowserClient) Printf(format string, args ...interface{}) {
log.Printf(fmt.Sprintf("Browser %s] %s", bc.SessionID, format), args...)
}
func (bc *BrowserClient) Println(args ...interface{}) {
log.Println(fmt.Sprintf("Browser %s] %s", bc.SessionID, fmt.Sprint(args...)))
}

View file

@ -1,27 +1,119 @@
package coordinator
import (
"log"
"errors"
"fmt"
"html/template"
"net/http"
"strings"
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/monitoring"
"github.com/giongto35/cloud-game/v2/pkg/service"
"github.com/giongto35/cloud-game/v3/pkg/config"
"github.com/giongto35/cloud-game/v3/pkg/logger"
"github.com/giongto35/cloud-game/v3/pkg/monitoring"
"github.com/giongto35/cloud-game/v3/pkg/network/httpx"
)
func New(conf coordinator.Config) (services service.Group) {
srv := NewServer(conf, games.NewLibWhitelisted(conf.Coordinator.Library, conf.Emulator))
httpSrv, err := NewHTTPServer(conf, func(mux *http.ServeMux) {
mux.HandleFunc("/ws", srv.WS)
mux.HandleFunc("/wso", srv.WSO)
type Coordinator struct {
hub *Hub
services [2]interface {
Run()
Stop() error
}
}
func New(conf config.CoordinatorConfig, log *logger.Logger) (*Coordinator, error) {
coordinator := &Coordinator{hub: NewHub(conf, log)}
h, err := NewHTTPServer(conf, log, func(mux *httpx.Mux) *httpx.Mux {
mux.HandleFunc("/ws", coordinator.hub.handleUserConnection())
mux.HandleFunc("/wso", coordinator.hub.handleWorkerConnection())
return mux
})
if err != nil {
log.Fatalf("http init fail: %v", err)
return nil, fmt.Errorf("http init fail: %w", err)
}
services.Add(srv, httpSrv)
coordinator.services[0] = h
if conf.Coordinator.Monitoring.IsEnabled() {
services.Add(monitoring.New(conf.Coordinator.Monitoring, httpSrv.GetHost(), "cord"))
coordinator.services[1] = monitoring.New(conf.Coordinator.Monitoring, h.GetHost(), log)
}
return
return coordinator, nil
}
func (c *Coordinator) Start() {
for _, s := range c.services {
if s != nil {
s.Run()
}
}
}
func (c *Coordinator) Stop() error {
var err error
for _, s := range c.services {
if s != nil {
err0 := s.Stop()
err = errors.Join(err, err0)
}
}
return err
}
func NewHTTPServer(conf config.CoordinatorConfig, log *logger.Logger, fnMux func(*httpx.Mux) *httpx.Mux) (*httpx.Server, error) {
return httpx.NewServer(
conf.Coordinator.Server.GetAddr(),
func(s *httpx.Server) httpx.Handler { return fnMux(s.Mux().Handle("/", index(conf, log))) },
httpx.WithServerConfig(conf.Coordinator.Server),
httpx.WithLogger(log),
)
}
func index(conf config.CoordinatorConfig, log *logger.Logger) httpx.Handler {
const indexHTML = "./web/index.html"
indexTpl := template.Must(template.ParseFiles(indexHTML))
// render index page with some tpl values
tplData := struct {
Analytics config.Analytics
Recording config.Recording
}{conf.Coordinator.Analytics, conf.Recording}
handler := func(tpl *template.Template, w httpx.ResponseWriter, r *httpx.Request) {
if err := tpl.Execute(w, tplData); err != nil {
log.Error().Err(err).Msg("error with the analytics template file")
}
}
h := httpx.FileServer("./web")
if conf.Coordinator.Debug {
log.Info().Msgf("Using auto-reloading index.html")
return httpx.HandlerFunc(func(w httpx.ResponseWriter, r *httpx.Request) {
if conf.Coordinator.Server.CacheControl != "" {
w.Header().Add("Cache-Control", conf.Coordinator.Server.CacheControl)
}
if conf.Coordinator.Server.FrameOptions != "" {
w.Header().Add("X-Frame-Options", conf.Coordinator.Server.FrameOptions)
}
if r.URL.Path == "/" || strings.HasSuffix(r.URL.Path, "/index.html") {
tpl := template.Must(template.ParseFiles(indexHTML))
handler(tpl, w, r)
return
}
h.ServeHTTP(w, r)
})
}
return httpx.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if conf.Coordinator.Server.CacheControl != "" {
w.Header().Add("Cache-Control", conf.Coordinator.Server.CacheControl)
}
if conf.Coordinator.Server.FrameOptions != "" {
w.Header().Add("X-Frame-Options", conf.Coordinator.Server.FrameOptions)
}
if r.URL.Path == "/" || strings.HasSuffix(r.URL.Path, "/index.html") {
handler(indexTpl, w, r)
return
}
h.ServeHTTP(w, r)
})
}

View file

@ -1,381 +0,0 @@
package coordinator
import (
"encoding/json"
"errors"
"log"
"math"
"net/http"
"strings"
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
"github.com/giongto35/cloud-game/v2/pkg/environment"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/ice"
"github.com/giongto35/cloud-game/v2/pkg/service"
"github.com/giongto35/cloud-game/v2/pkg/util"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
)
type Server struct {
service.Service
cfg coordinator.Config
// games library
library games.GameLibrary
// roomToWorker map roomID to workerID
roomToWorker map[string]string
// workerClients are the map workerID to worker Client
workerClients map[string]*WorkerClient
// browserClients are the map sessionID to browser Client
browserClients map[string]*BrowserClient
}
var upgrader = websocket.Upgrader{}
func NewServer(cfg coordinator.Config, library games.GameLibrary) *Server {
// scan the lib right away
library.Scan()
return &Server{
cfg: cfg,
library: library,
// Mapping roomID to server
roomToWorker: map[string]string{},
// Mapping workerID to worker
workerClients: map[string]*WorkerClient{},
// Mapping sessionID to browser
browserClients: map[string]*BrowserClient{},
}
}
// WSO handles all connections from a new worker to coordinator
func (s *Server) WSO(w http.ResponseWriter, r *http.Request) {
log.Println("Coordinator: A worker is connecting...")
connRt, err := GetConnectionRequest(r.URL.Query().Get("data"))
if err != nil {
log.Printf("Coordinator: got a malformed request: %v", err.Error())
return
}
if connRt.PingAddr == "" {
log.Printf("Warning! Ping address is not set.")
}
if s.cfg.Coordinator.Server.Https && !connRt.IsHTTPS {
log.Printf("Warning! Unsecure connection. The worker may not work properly without HTTPS on its side!")
}
// be aware of ReadBufferSize, WriteBufferSize (default 4096)
// https://pkg.go.dev/github.com/gorilla/websocket?tab=doc#Upgrader
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Coordinator: [!] WS upgrade:", err)
return
}
// Generate workerID
var workerID string
for {
workerID = uuid.Must(uuid.NewV4()).String()
// check duplicate
if _, ok := s.workerClients[workerID]; !ok {
break
}
}
// Create a workerClient instance
wc := NewWorkerClient(c, workerID)
wc.Println("Generated worker ID")
wc.Zone = connRt.Zone
wc.PingServer = connRt.PingAddr
// Register to workersClients map the client connection
address := util.GetRemoteAddress(c)
public := util.IsPublicIP(address)
wc.Printf("addr: %v | zone: %v | pub: %v | ping: %v", address, wc.Zone, public, wc.PingServer)
// In case worker and coordinator in the same host
if !public && s.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)
address = util.GetHostPublicIP()
wc.Printf("Find public address: %s", address)
if address == "" || !util.IsPublicIP(address) {
// Skip this worker because we cannot find public IP
wc.Println("[!] Unable to find public address, reject worker")
return
}
}
// Create a workerClient instance
wc.Address = address
wc.StunTurnServer = ice.ToJson(s.cfg.Webrtc.IceServers, ice.Replacement{From: "server-ip", To: address})
// Attach to Server instance with workerID, add defer
s.workerClients[workerID] = wc
defer s.cleanWorker(wc, workerID)
wc.Send(api.ServerIdPacket(workerID), nil)
s.workerRoutes(wc)
wc.Listen()
}
// WSO handles all connections from user/frontend to coordinator
func (s *Server) WS(w http.ResponseWriter, r *http.Request) {
log.Println("Coordinator: A user is connecting...")
defer func() {
if r := recover(); r != nil {
log.Println("Warn: Something wrong. Recovered in ", r)
}
}()
// be aware of ReadBufferSize, WriteBufferSize (default 4096)
// https://pkg.go.dev/github.com/gorilla/websocket?tab=doc#Upgrader
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Coordinator: [!] WS upgrade:", err)
return
}
// Generate sessionID for browserClient
var sessionID string
for {
sessionID = uuid.Must(uuid.NewV4()).String()
// check duplicate
if _, ok := s.browserClients[sessionID]; !ok {
break
}
}
// Create browserClient instance
bc := NewBrowserClient(c, sessionID)
bc.Println("Generated worker ID")
// Run browser listener first (to capture ping)
go bc.Listen()
/* Create a session - mapping browserClient with workerClient */
var wc *WorkerClient
// get roomID if it is embeded in request. Server will pair the frontend with the server running the room. It only happens when we are trying to access a running room over share link.
// TODO: Update link to the wiki
roomID := r.URL.Query().Get("room_id")
// zone param is to pick worker in that zone only
// if there is no zone param, we can pic
userZone := r.URL.Query().Get("zone")
bc.Printf("Get Room %s Zone %s From URL %v", roomID, userZone, r.URL)
if roomID != "" {
bc.Printf("Detected roomID %v from URL", roomID)
if workerID, ok := s.roomToWorker[roomID]; ok {
wc = s.workerClients[workerID]
if userZone != "" && wc.Zone != userZone {
// if there is zone param, we need to ensure ther worker in that zone
// if not we consider the room is missing
wc = nil
} else {
bc.Printf("Found running server with id=%v client=%v", workerID, wc)
}
}
}
// If there is no existing server to connect to, we find the best possible worker for the frontend
if wc == nil {
// Get best server for frontend to connect to
wc, err = s.getBestWorkerClient(bc, userZone)
if err != nil {
return
}
}
// Assign available worker to browserClient
bc.WorkerID = wc.WorkerID
wc.ChangeUserQuantityBy(1)
defer wc.ChangeUserQuantityBy(-1)
// Everything is cool
// Attach to Server instance with sessionID
s.browserClients[sessionID] = bc
defer s.cleanBrowser(bc, sessionID)
// Routing browserClient message
s.useragentRoutes(bc)
bc.Send(cws.WSPacket{
ID: "init",
Data: createInitPackage(wc.StunTurnServer, s.library.GetAll()),
}, nil)
// If peerconnection is done (client.Done is signalled), we close peerconnection
<-bc.Done
// Notify worker to clean session
wc.Send(api.TerminateSessionPacket(sessionID), nil)
}
func (s *Server) getBestWorkerClient(client *BrowserClient, zone string) (*WorkerClient, error) {
conf := s.cfg.Coordinator
if conf.DebugHost != "" {
client.Println("Connecting to debug host instead prod servers", conf.DebugHost)
wc := s.getWorkerFromAddress(conf.DebugHost)
if wc != nil {
return wc, nil
}
// if there is not debugHost, continue usual flow
client.Println("Not found, connecting to all available servers")
}
workerClients := s.getAvailableWorkers()
serverID, err := s.findBestServerFromBrowser(workerClients, client, zone)
if err != nil {
log.Println(err)
return nil, err
}
return s.workerClients[serverID], nil
}
// getAvailableWorkers returns the list of available worker
func (s *Server) getAvailableWorkers() map[string]*WorkerClient {
workerClients := map[string]*WorkerClient{}
for k, w := range s.workerClients {
if w.HasGameSlot() {
workerClients[k] = w
}
}
return workerClients
}
// getWorkerFromAddress returns the worker has given address
func (s *Server) getWorkerFromAddress(address string) *WorkerClient {
for _, w := range s.workerClients {
if w.HasGameSlot() && w.Address == address {
return w
}
}
return nil
}
// findBestServerFromBrowser returns the best server for a session
// All workers addresses are sent to user and user will ping to get latency
func (s *Server) findBestServerFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient, zone string) (string, error) {
// TODO: Find best Server by latency, currently return by ping
if len(workerClients) == 0 {
return "", errors.New("no server found")
}
latencies := s.getLatencyMapFromBrowser(workerClients, client)
client.Println("Latency map", latencies)
if len(latencies) == 0 {
return "", errors.New("no server found")
}
var bestWorker *WorkerClient
var minLatency int64 = math.MaxInt64
// get the worker with lowest latency to user
for wc, l := range latencies {
if zone != "" && wc.Zone != zone {
// skip worker not in the zone if zone param is given
continue
}
if l < minLatency {
bestWorker = wc
minLatency = l
}
}
return bestWorker.WorkerID, nil
}
// getLatencyMapFromBrowser get all latencies from worker to user
func (s *Server) getLatencyMapFromBrowser(workerClients map[string]*WorkerClient, client *BrowserClient) map[*WorkerClient]int64 {
var workersList []*WorkerClient
var addressList []string
uniqueAddresses := map[string]bool{}
latencyMap := map[*WorkerClient]int64{}
// addressList is the list of worker addresses
for _, workerClient := range workerClients {
if _, ok := uniqueAddresses[workerClient.PingServer]; !ok {
addressList = append(addressList, workerClient.PingServer)
}
uniqueAddresses[workerClient.PingServer] = true
workersList = append(workersList, workerClient)
}
// send this address to user and get back latency
client.Println("Send sync", addressList, strings.Join(addressList, ","))
data := client.SyncSend(cws.WSPacket{
ID: "checkLatency",
Data: strings.Join(addressList, ","),
})
respLatency := map[string]int64{}
err := json.Unmarshal([]byte(data.Data), &respLatency)
if err != nil {
log.Println(err)
return latencyMap
}
for _, workerClient := range workersList {
if latency, ok := respLatency[workerClient.PingServer]; ok {
latencyMap[workerClient] = latency
}
}
return latencyMap
}
// cleanBrowser is called when a browser is disconnected
func (s *Server) cleanBrowser(bc *BrowserClient, sessionID string) {
bc.Println("Disconnect from coordinator")
delete(s.browserClients, sessionID)
bc.Close()
}
// cleanWorker is called when a worker is disconnected
// connection from worker to coordinator is also closed
func (s *Server) cleanWorker(wc *WorkerClient, workerID string) {
wc.Println("Unregister worker from coordinator")
// Remove workerID from workerClients
delete(s.workerClients, workerID)
// Clean all rooms connecting to that server
for roomID, roomServer := range s.roomToWorker {
if roomServer == workerID {
wc.Printf("Remove room %s", roomID)
delete(s.roomToWorker, roomID)
}
}
wc.Close()
}
// createInitPackage returns serverhost + game list in encoded wspacket format
// This package will be sent to initialize
func createInitPackage(stunturn string, games []games.GameMetadata) string {
var gameName []string
for _, game := range games {
gameName = append(gameName, game.Name)
}
initPackage := append([]string{stunturn}, gameName...)
encodedList, _ := json.Marshal(initPackage)
return string(encodedList)
}

View file

@ -1,47 +0,0 @@
package coordinator
import (
"html/template"
"log"
"net/http"
"github.com/giongto35/cloud-game/v2/pkg/config/coordinator"
"github.com/giongto35/cloud-game/v2/pkg/network/httpx"
)
func NewHTTPServer(conf coordinator.Config, fnMux func(mux *http.ServeMux)) (*httpx.Server, error) {
return httpx.NewServer(
conf.Coordinator.Server.GetAddr(),
func(*httpx.Server) http.Handler {
h := http.NewServeMux()
h.Handle("/", index(conf))
h.Handle("/static/", static("./web"))
fnMux(h)
return h
},
httpx.WithServerConfig(conf.Coordinator.Server),
)
}
func index(conf coordinator.Config) http.Handler {
tpl, err := template.ParseFiles("./web/index.html")
if err != nil {
log.Fatal(err)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// return 404 on unknown
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
// render index page with some tpl values
if err = tpl.Execute(w, conf.Coordinator.Analytics); err != nil {
log.Fatal(err)
}
})
}
func static(dir string) http.Handler {
return http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))
}

337
pkg/coordinator/hub.go Normal file
View file

@ -0,0 +1,337 @@
package coordinator
import (
"bytes"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"github.com/giongto35/cloud-game/v3/pkg/api"
"github.com/giongto35/cloud-game/v3/pkg/com"
"github.com/giongto35/cloud-game/v3/pkg/config"
"github.com/giongto35/cloud-game/v3/pkg/logger"
)
type Connection interface {
Disconnect()
Id() com.Uid
ProcessPackets(func(api.In[com.Uid]) error) chan struct{}
Send(api.PT, any) ([]byte, error)
Notify(api.PT, any)
}
type Hub struct {
conf config.CoordinatorConfig
log *logger.Logger
users com.NetMap[com.Uid, *User]
workers com.NetMap[com.Uid, *Worker]
}
func NewHub(conf config.CoordinatorConfig, log *logger.Logger) *Hub {
return &Hub{
conf: conf,
users: com.NewNetMap[com.Uid, *User](),
workers: com.NewNetMap[com.Uid, *Worker](),
log: log,
}
}
// handleUserConnection handles all connections from user/frontend.
func (h *Hub) handleUserConnection() http.HandlerFunc {
var connector com.Server
connector.Origin(h.conf.Coordinator.Origin.UserWs)
log := h.log.Extend(h.log.With().
Str(logger.ClientField, "u").
Str(logger.DirectionField, logger.MarkIn),
)
return func(w http.ResponseWriter, r *http.Request) {
h.log.Debug().Msgf("Handshake %v", r.Host)
conn, err := connector.Connect(w, r)
if err != nil {
h.log.Error().Err(err).Msg("user connection fail")
return
}
user := NewUser(conn, log)
defer h.users.RemoveDisconnect(user)
done := user.HandleRequests(h, h.conf)
params := r.URL.Query()
worker := h.findWorkerFor(user, params, h.log.Extend(h.log.With().Str("cid", user.Id().Short())))
if worker == nil {
user.Notify(api.ErrNoFreeSlots, "")
h.log.Info().Msg("no free workers")
return
}
// Link the user to the selected worker. Slot reservation is handled later
// on game start; this keeps connections lightweight and lets deep-link
// joins share a worker without consuming its single game slot.
user.w = worker
h.users.Add(user)
apps := worker.AppNames()
list := make([]api.AppMeta, len(apps))
for i := range apps {
list[i] = api.AppMeta{Alias: apps[i].Alias, Title: apps[i].Name, System: apps[i].System}
}
user.InitSession(worker.Id().String(), h.conf.Webrtc.IceServers, list)
log.Info().Str(logger.DirectionField, logger.MarkPlus).Msgf("user %s", user.Id())
<-done
}
}
func RequestToHandshake(data string) (*api.ConnectionRequest[com.Uid], error) {
if data == "" {
return nil, api.ErrMalformed
}
handshake, err := api.UnwrapChecked[api.ConnectionRequest[com.Uid]](base64.URLEncoding.DecodeString(data))
if err != nil || handshake == nil {
return nil, fmt.Errorf("%w (%v)", err, handshake)
}
return handshake, nil
}
// handleWorkerConnection handles all connections from a new worker to coordinator.
func (h *Hub) handleWorkerConnection() http.HandlerFunc {
var connector com.Server
connector.Origin(h.conf.Coordinator.Origin.WorkerWs)
log := h.log.Extend(h.log.With().
Str(logger.ClientField, "w").
Str(logger.DirectionField, logger.MarkIn),
)
h.log.Debug().Msgf("WS max message size: %vb", h.conf.Coordinator.MaxWsSize)
return func(w http.ResponseWriter, r *http.Request) {
h.log.Debug().Msgf("Handshake %v", r.Host)
handshake, err := RequestToHandshake(r.URL.Query().Get(api.DataQueryParam))
if err != nil {
h.log.Error().Err(err).Msg("handshake fail")
return
}
if handshake.PingURL == "" {
h.log.Warn().Msg("Ping address is not set")
}
if h.conf.Coordinator.Server.Https && !handshake.IsHTTPS {
h.log.Warn().Msg("Unsecure worker connection. Unsecure to secure may be bad.")
}
// set connection uid from the handshake
if handshake.Id != com.NilUid {
h.log.Debug().Msgf("Worker uid will be set to %v", handshake.Id)
}
conn, err := connector.Connect(w, r)
if err != nil {
log.Error().Err(err).Msg("worker connection fail")
return
}
conn.SetMaxReadSize(h.conf.Coordinator.MaxWsSize)
worker := NewWorker(conn, *handshake, log)
defer h.workers.RemoveDisconnect(worker)
done := worker.HandleRequests(&h.users)
h.workers.Add(worker)
log.Info().
Str(logger.DirectionField, logger.MarkPlus).
Msgf("worker %s", worker.PrintInfo())
<-done
}
}
func (h *Hub) GetServerList() (r []api.Server) {
debug := h.conf.Coordinator.Debug
for w := range h.workers.Values() {
server := api.Server{
Addr: w.Addr,
Id: w.Id(),
IsBusy: !w.HasSlot(),
Machine: string(w.Id().Machine()),
PingURL: w.PingServer,
Port: w.Port,
Tag: w.Tag,
Zone: w.Zone,
}
if debug {
server.Room = w.RoomId
}
r = append(r, server)
}
return
}
// findWorkerFor searches a free worker for the user depending on
// various conditions.
func (h *Hub) findWorkerFor(usr *User, q url.Values, log *logger.Logger) *Worker {
log.Debug().Msg("Search available workers")
roomIdRaw := q.Get(api.RoomIdQueryParam)
sessionId, deepRoomId := api.ExplodeDeepLink(roomIdRaw)
roomId := roomIdRaw
if deepRoomId != "" {
roomId = deepRoomId
}
zone := q.Get(api.ZoneQueryParam)
wid := q.Get(api.WorkerIdParam)
var worker *Worker
if wid != "" {
if worker = h.findWorkerById(wid, h.conf.Coordinator.Debug); worker != nil {
log.Debug().Msgf("Worker with id: %v has been found", wid)
return worker
} else {
return nil
}
}
if worker = h.findWorkerByRoom(roomIdRaw, roomId, zone); worker != nil {
log.Debug().Str("room", roomId).Msg("An existing worker has been found")
} else if worker = h.findWorkerByPreviousRoom(sessionId); worker != nil {
log.Debug().Msgf("Worker %v with the previous room: %v is found", wid, roomId)
} else {
switch h.conf.Coordinator.Selector {
case config.SelectByPing:
log.Debug().Msgf("Searching fastest free worker...")
if worker = h.findFastestWorker(zone,
func(servers []string) (map[string]int64, error) { return usr.CheckLatency(servers) }); worker != nil {
log.Debug().Msg("The fastest worker has been found")
}
default:
log.Debug().Msgf("Searching any free worker...")
if worker = h.find1stFreeWorker(zone); worker != nil {
log.Debug().Msgf("Found next free worker")
}
}
}
return worker
}
func (h *Hub) findWorkerByPreviousRoom(id string) *Worker {
if id == "" {
return nil
}
w, _ := h.workers.FindBy(func(w *Worker) bool {
// session and room id are the same
return w.HadSession(id) && w.HasSlot()
})
return w
}
func (h *Hub) findWorkerByRoom(id string, deepId string, region string) *Worker {
if id == "" && deepId == "" {
return nil
}
// if there is zone param, we need to ensure the worker in that zone,
// if not we consider the room is missing
w, _ := h.workers.FindBy(func(w *Worker) bool {
matchId := w.RoomId == id
if !matchId && deepId != "" {
matchId = w.RoomId == deepId
}
return matchId && w.In(region)
})
return w
}
func (h *Hub) getAvailableWorkers(region string) []*Worker {
var workers []*Worker
for w := range h.workers.Values() {
if w.HasSlot() && w.In(region) {
workers = append(workers, w)
}
}
return workers
}
func (h *Hub) find1stFreeWorker(region string) *Worker {
workers := h.getAvailableWorkers(region)
if len(workers) > 0 {
return workers[0]
}
return nil
}
// findFastestWorker returns the best server for a session.
// All workers addresses are sent to user and user will ping to get latency.
// !to rewrite
func (h *Hub) findFastestWorker(region string, fn func(addresses []string) (map[string]int64, error)) *Worker {
workers := h.getAvailableWorkers(region)
if len(workers) == 0 {
return nil
}
var addresses []string
group := map[string][]struct{}{}
for _, w := range workers {
if _, ok := group[w.PingServer]; !ok {
addresses = append(addresses, w.PingServer)
}
group[w.PingServer] = append(group[w.PingServer], struct{}{})
}
latencies, err := fn(addresses)
if len(latencies) == 0 || err != nil {
return nil
}
workers = h.getAvailableWorkers(region)
if len(workers) == 0 {
return nil
}
var bestWorker *Worker
var minLatency int64 = 1<<31 - 1
// get a worker with the lowest latency
for addr, ping := range latencies {
if ping < minLatency {
for _, w := range workers {
if w.PingServer == addr {
bestWorker = w
}
}
minLatency = ping
}
}
return bestWorker
}
func (h *Hub) findWorkerById(id string, useAllWorkers bool) *Worker {
if id == "" {
return nil
}
uid, err := com.UidFromString(id)
if err != nil {
return nil
}
for _, w := range h.getAvailableWorkers("") {
if w.Id() == com.NilUid {
continue
}
if useAllWorkers {
if uid == w.Id() {
return w
}
} else {
// select any worker on the same machine when workers are grouped on the client
if bytes.Equal(uid.Machine(), w.Id().Machine()) {
return w
}
}
}
return nil
}

View file

@ -1,75 +0,0 @@
package coordinator
import (
"encoding/base64"
"encoding/json"
"log"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
)
func (wc *WorkerClient) handleHeartbeat() cws.PacketHandler {
return func(resp cws.WSPacket) cws.WSPacket {
return resp
}
}
func GetConnectionRequest(data string) (api.ConnectionRequest, error) {
req := api.ConnectionRequest{}
if data == "" {
return req, nil
}
decodeString, err := base64.URLEncoding.DecodeString(data)
if err != nil {
return req, err
}
err = json.Unmarshal(decodeString, &req)
return req, err
}
// handleRegisterRoom event from a worker, when worker created a new room.
// RoomID is global so it is managed by coordinator.
func (wc *WorkerClient) handleRegisterRoom(s *Server) cws.PacketHandler {
return func(resp cws.WSPacket) cws.WSPacket {
log.Printf("Coordinator: Received registerRoom room %s from worker %s", resp.Data, wc.WorkerID)
s.roomToWorker[resp.Data] = wc.WorkerID
log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
return api.RegisterRoomPacket(api.NoData)
}
}
// handleGetRoom returns the server ID based on requested roomID.
func (wc *WorkerClient) handleGetRoom(s *Server) cws.PacketHandler {
return func(resp cws.WSPacket) cws.WSPacket {
log.Println("Coordinator: Received a get room request")
log.Println("Result: ", s.roomToWorker[resp.Data])
return api.GetRoomPacket(s.roomToWorker[resp.Data])
}
}
// handleCloseRoom event from a worker, when worker close a room.
func (wc *WorkerClient) handleCloseRoom(s *Server) cws.PacketHandler {
return func(resp cws.WSPacket) cws.WSPacket {
log.Printf("Coordinator: Received closeRoom room %s from worker %s", resp.Data, wc.WorkerID)
delete(s.roomToWorker, resp.Data)
log.Printf("Coordinator: Current room list is: %+v", s.roomToWorker)
return api.CloseRoomPacket(api.NoData)
}
}
// handleIceCandidate passes an ICE candidate (WebRTC) to the browser.
func (wc *WorkerClient) handleIceCandidate(s *Server) cws.PacketHandler {
return func(resp cws.WSPacket) cws.WSPacket {
wc.Println("Received IceCandidate from worker -> relay to browser")
bc, ok := s.browserClients[resp.SessionID]
if ok {
// Remove SessionID while sending back to browser
resp.SessionID = ""
bc.Send(resp, nil)
} else {
wc.Println("Error: unknown SessionID:", resp.SessionID)
}
return cws.EmptyPacket
}
}

View file

@ -1,32 +0,0 @@
package coordinator
import "github.com/giongto35/cloud-game/v2/pkg/cws/api"
// workerRoutes adds all worker request routes.
func (s *Server) workerRoutes(wc *WorkerClient) {
if wc == nil {
return
}
wc.Receive(api.Heartbeat, wc.handleHeartbeat())
wc.Receive(api.RegisterRoom, wc.handleRegisterRoom(s))
wc.Receive(api.GetRoom, wc.handleGetRoom(s))
wc.Receive(api.CloseRoom, wc.handleCloseRoom(s))
wc.Receive(api.IceCandidate, wc.handleIceCandidate(s))
}
// useragentRoutes adds all useragent (browser) request routes.
func (s *Server) useragentRoutes(bc *BrowserClient) {
if bc == nil {
return
}
bc.Receive(api.Heartbeat, bc.handleHeartbeat())
bc.Receive(api.InitWebrtc, bc.handleInitWebrtc(s))
bc.Receive(api.Answer, bc.handleAnswer(s))
bc.Receive(api.IceCandidate, bc.handleIceCandidate(s))
bc.Receive(api.GameStart, bc.handleGameStart(s))
bc.Receive(api.GameQuit, bc.handleGameQuit(s))
bc.Receive(api.GameSave, bc.handleGameSave(s))
bc.Receive(api.GameLoad, bc.handleGameLoad(s))
bc.Receive(api.GamePlayerSelect, bc.handleGamePlayerSelect(s))
bc.Receive(api.GameMultitap, bc.handleGameMultitap(s))
}

81
pkg/coordinator/user.go Normal file
View file

@ -0,0 +1,81 @@
package coordinator
import (
"github.com/giongto35/cloud-game/v3/pkg/api"
"github.com/giongto35/cloud-game/v3/pkg/com"
"github.com/giongto35/cloud-game/v3/pkg/config"
"github.com/giongto35/cloud-game/v3/pkg/logger"
)
type User struct {
Connection
w *Worker // linked worker
log *logger.Logger
}
type HasServerInfo interface {
GetServerList() []api.Server
}
func NewUser(sock *com.Connection, log *logger.Logger) *User {
conn := com.NewConnection[api.PT, api.In[com.Uid], api.Out, *api.Out](sock, com.NewUid(), log)
return &User{
Connection: conn,
log: log.Extend(log.With().
Str(logger.ClientField, logger.MarkNone).
Str(logger.DirectionField, logger.MarkNone).
Str("cid", conn.Id().Short())),
}
}
func (u *User) Bind(w *Worker) bool {
u.w = w
// Binding only links the worker; slot reservation is handled lazily on
// game start to avoid blocking deep-link joins or parallel connections
// that haven't started a game yet.
return true
}
func (u *User) Disconnect() {
u.Connection.Disconnect()
if u.w != nil {
u.w.TerminateSession(u.Id().String())
}
}
func (u *User) HandleRequests(info HasServerInfo, conf config.CoordinatorConfig) chan struct{} {
return u.ProcessPackets(func(x api.In[com.Uid]) (err error) {
switch x.T {
case api.WebrtcInit:
if u.w != nil {
u.HandleWebrtcInit()
}
case api.WebrtcAnswer:
err = api.Do(x, u.HandleWebrtcAnswer)
case api.WebrtcIce:
err = api.Do(x, u.HandleWebrtcIceCandidate)
case api.StartGame:
err = api.Do(x, func(d api.GameStartUserRequest) { u.HandleStartGame(d, conf) })
case api.QuitGame:
err = api.Do(x, u.HandleQuitGame)
case api.SaveGame:
err = u.HandleSaveGame()
case api.LoadGame:
err = u.HandleLoadGame()
case api.ChangePlayer:
err = api.Do(x, u.HandleChangePlayer)
case api.ResetGame:
err = api.Do(x, u.HandleResetGame)
case api.RecordGame:
if !conf.Recording.Enabled {
return api.ErrForbidden
}
err = api.Do(x, u.HandleRecordGame)
case api.GetWorkerList:
u.handleGetWorkerList(conf.Coordinator.Debug, info)
default:
u.log.Warn().Msgf("Unknown packet: %+v", x)
}
return
})
}

View file

@ -1,212 +0,0 @@
package coordinator
import (
"errors"
"fmt"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/giongto35/cloud-game/v2/pkg/cws/api"
"github.com/giongto35/cloud-game/v2/pkg/games"
"github.com/giongto35/cloud-game/v2/pkg/session"
)
func (bc *BrowserClient) handleHeartbeat() cws.PacketHandler {
return func(resp cws.WSPacket) cws.WSPacket { return resp }
}
func (bc *BrowserClient) handleInitWebrtc(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
// initWebrtc now only sends signal to worker, asks it to createOffer
bc.Printf("Received init_webrtc request -> relay to worker: %s", bc.WorkerID)
// relay request to target worker
// worker creates a PeerConnection, and createOffer
// send SDP back to browser
resp.SessionID = bc.SessionID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
sdp := wc.SyncSend(resp)
bc.Println("Received SDP from worker -> sending back to browser")
return sdp
}
}
func (bc *BrowserClient) handleAnswer(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
// contains SDP of browser createAnswer
// forward to worker
bc.Println("Received browser answered SDP -> relay to worker")
resp.SessionID = bc.SessionID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
wc.Send(resp, nil)
// no need to response
return cws.EmptyPacket
}
}
func (bc *BrowserClient) handleIceCandidate(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
// contains ICE candidate of browser
// forward to worker
bc.Println("Received IceCandidate from browser -> relay to worker")
resp.SessionID = bc.SessionID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
wc.Send(resp, nil)
return cws.EmptyPacket
}
}
func (bc *BrowserClient) handleGameStart(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
bc.Println("Received start request from a browser -> relay to worker")
// TODO: Async
resp.SessionID = bc.SessionID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
// +injects game data into the original game request
gameStartCall, err := newGameStartCall(resp.RoomID, resp.Data, o.library)
if err != nil {
return cws.EmptyPacket
}
if packet, err := gameStartCall.To(); err != nil {
return cws.EmptyPacket
} else {
resp.Data = packet
}
workerResp := wc.SyncSend(resp)
// Response from worker contains initialized roomID. Set roomID to the session
bc.RoomID = workerResp.RoomID
bc.Println("Received room response from browser: ", workerResp.RoomID)
return workerResp
}
}
func (bc *BrowserClient) handleGameQuit(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
bc.Println("Received quit request from a browser -> relay to worker")
// TODO: Async
resp.SessionID = bc.SessionID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
// Send but, waiting
wc.SyncSend(resp)
return cws.EmptyPacket
}
}
func (bc *BrowserClient) handleGameSave(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
bc.Println("Received save request from a browser -> relay to worker")
// TODO: Async
resp.SessionID = bc.SessionID
resp.RoomID = bc.RoomID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
resp = wc.SyncSend(resp)
return resp
}
}
func (bc *BrowserClient) handleGameLoad(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
bc.Println("Received load request from a browser -> relay to worker")
// TODO: Async
resp.SessionID = bc.SessionID
resp.RoomID = bc.RoomID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
resp = wc.SyncSend(resp)
return resp
}
}
func (bc *BrowserClient) handleGamePlayerSelect(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
bc.Println("Received update player index request from a browser -> relay to worker")
// TODO: Async
resp.SessionID = bc.SessionID
resp.RoomID = bc.RoomID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
resp = wc.SyncSend(resp)
return resp
}
}
func (bc *BrowserClient) handleGameMultitap(o *Server) cws.PacketHandler {
return func(resp cws.WSPacket) (req cws.WSPacket) {
bc.Println("Received multitap request from a browser -> relay to worker")
// TODO: Async
resp.SessionID = bc.SessionID
resp.RoomID = bc.RoomID
wc, ok := o.workerClients[bc.WorkerID]
if !ok {
return cws.EmptyPacket
}
resp = wc.SyncSend(resp)
return resp
}
}
// newGameStartCall gathers data for a new game start call of the worker
func newGameStartCall(roomId string, data string, library games.GameLibrary) (api.GameStartCall, error) {
request := api.GameStartRequest{}
if err := request.From(data); err != nil {
return api.GameStartCall{}, errors.New("invalid request")
}
// the name of the game either in the `room id` field or
// it's in the initial request
game := request.GameName
if roomId != "" {
// ! should be moved into coordinator
name := session.GetGameNameFromRoomID(roomId)
if name == "" {
return api.GameStartCall{}, errors.New("couldn't decode game name from the room id")
}
game = name
}
gameInfo := library.FindGameByName(game)
if gameInfo.Path == "" {
return api.GameStartCall{}, fmt.Errorf("couldn't find game info for the game %v", game)
}
return api.GameStartCall{
Name: gameInfo.Name,
Base: gameInfo.Base,
Path: gameInfo.Path,
Type: gameInfo.Type,
}, nil
}

View file

@ -0,0 +1,38 @@
package coordinator
import (
"unsafe"
"github.com/giongto35/cloud-game/v3/pkg/api"
"github.com/giongto35/cloud-game/v3/pkg/config"
)
// CheckLatency sends a list of server addresses to the user
// and waits get back this list with tested ping times for each server.
func (u *User) CheckLatency(req api.CheckLatencyUserResponse) (api.CheckLatencyUserRequest, error) {
dat, err := api.UnwrapChecked[api.CheckLatencyUserRequest](u.Send(api.CheckLatency, req))
if dat == nil {
return api.CheckLatencyUserRequest{}, err
}
return *dat, nil
}
// InitSession signals the user that the app is ready to go.
func (u *User) InitSession(wid string, ice []config.IceServer, games []api.AppMeta) {
u.Notify(api.InitSession, api.InitSessionUserResponse{
Ice: *(*[]api.IceServer)(unsafe.Pointer(&ice)), // don't do this at home
Games: games,
Wid: wid,
})
}
// SendWebrtcOffer sends SDP offer back to the user.
func (u *User) SendWebrtcOffer(sdp string) { u.Notify(api.WebrtcOffer, sdp) }
// SendWebrtcIceCandidate sends remote ICE candidate back to the user.
func (u *User) SendWebrtcIceCandidate(candidate string) { u.Notify(api.WebrtcIce, candidate) }
// StartGame signals the user that everything is ready to start a game.
func (u *User) StartGame(av *api.AppVideoInfo, kbMouse bool) {
u.Notify(api.StartGame, api.GameStartUserResponse{RoomId: u.w.RoomId, Av: av, KbMouse: kbMouse})
}

View file

@ -0,0 +1,196 @@
package coordinator
import (
"sort"
"time"
"github.com/giongto35/cloud-game/v3/pkg/api"
"github.com/giongto35/cloud-game/v3/pkg/config"
)
func (u *User) HandleWebrtcInit() {
uid := u.Id().String()
resp, err := u.w.WebrtcInit(uid)
if err != nil || resp == nil || *resp == api.EMPTY {
u.log.Error().Err(err).Msg("malformed WebRTC init response")
return
}
u.SendWebrtcOffer(string(*resp))
}
func (u *User) HandleWebrtcAnswer(rq api.WebrtcAnswerUserRequest) {
u.w.WebrtcAnswer(u.Id().String(), string(rq))
}
func (u *User) HandleWebrtcIceCandidate(rq api.WebrtcUserIceCandidate) {
u.w.WebrtcIceCandidate(u.Id().String(), string(rq))
}
func (u *User) HandleStartGame(rq api.GameStartUserRequest, conf config.CoordinatorConfig) {
// Worker slot / room gating:
// - If the worker is BUSY (no free slot), we must not create another room.
// * If the worker has already reported a room id, only allow requests
// for that same room (deep-link joins / reloads).
// * If the worker hasn't reported a room yet, deny any new StartGame to
// avoid racing concurrent room creation on the worker.
// * When the user is starting a NEW game (empty room id), we give the
// worker a short grace period to close the previous room and free the
// slot before rejecting with "no slots".
// - If the worker is FREE, reserve the slot lazily before starting the
// game; the room id (if any) comes from the request / worker.
// Grace period: when there's no room id in the request (new game) but the
// worker still appears busy, wait a bit for the previous room to close.
if rq.RoomId == "" && !u.w.HasSlot() {
const waitTotal = 3 * time.Second
const step = 100 * time.Millisecond
waited := time.Duration(0)
for waited < waitTotal {
if u.w.HasSlot() {
break
}
time.Sleep(step)
waited += step
}
}
busy := !u.w.HasSlot()
if busy {
if u.w.RoomId == "" {
u.Notify(api.ErrNoFreeSlots, "")
return
}
if rq.RoomId == "" {
// No room id but worker is busy -> assume user wants to continue
// the existing room instead of starting a parallel game.
rq.RoomId = u.w.RoomId
} else if rq.RoomId != u.w.RoomId {
u.Notify(api.ErrNoFreeSlots, "")
return
}
} else {
// Worker is free: try to reserve the single slot for this new room.
if !u.w.TryReserve() {
u.Notify(api.ErrNoFreeSlots, "")
return
}
}
startGameResp, err := u.w.StartGame(u.Id().String(), rq)
if err != nil || startGameResp == nil {
u.log.Error().Err(err).Msg("malformed game start response")
return
}
if startGameResp.Rid == "" {
u.log.Error().Msg("there is no room")
return
}
u.log.Info().Str("id", startGameResp.Rid).Msg("Received room response from worker")
u.StartGame(startGameResp.AV, startGameResp.KbMouse)
// send back recording status
if conf.Recording.Enabled && rq.Record {
u.Notify(api.RecordGame, api.OK)
}
}
func (u *User) HandleQuitGame(rq api.GameQuitRequest) {
if rq.Rid == u.w.RoomId {
u.w.QuitGame(u.Id().String())
}
}
func (u *User) HandleResetGame(rq api.ResetGameRequest) {
if rq.Rid != u.w.RoomId {
return
}
u.w.ResetGame(u.Id().String())
}
func (u *User) HandleSaveGame() error {
resp, err := u.w.SaveGame(u.Id().String())
if err != nil {
return err
}
if *resp == api.OK {
if id, _ := api.ExplodeDeepLink(u.w.RoomId); id != "" {
u.w.AddSession(id)
}
}
u.Notify(api.SaveGame, resp)
return nil
}
func (u *User) HandleLoadGame() error {
resp, err := u.w.LoadGame(u.Id().String())
if err != nil {
return err
}
u.Notify(api.LoadGame, resp)
return nil
}
func (u *User) HandleChangePlayer(rq api.ChangePlayerUserRequest) {
resp, err := u.w.ChangePlayer(u.Id().String(), int(rq))
// !to make it a little less convoluted
if err != nil || resp == nil || *resp == -1 {
u.log.Error().Err(err).Msgf("player select fail, req: %v", rq)
return
}
u.Notify(api.ChangePlayer, rq)
}
func (u *User) HandleRecordGame(rq api.RecordGameRequest) {
if u.w == nil {
return
}
u.log.Debug().Msgf("??? room: %v, rec: %v user: %v", u.w.RoomId, rq.Active, rq.User)
if u.w.RoomId == "" {
u.log.Error().Msg("Recording in the empty room is not allowed!")
return
}
resp, err := u.w.RecordGame(u.Id().String(), rq.Active, rq.User)
if err != nil {
u.log.Error().Err(err).Msg("malformed game record request")
return
}
u.Notify(api.RecordGame, resp)
}
func (u *User) handleGetWorkerList(debug bool, info HasServerInfo) {
response := api.GetWorkerListResponse{}
servers := info.GetServerList()
if debug {
response.Servers = servers
} else {
unique := map[string]*api.Server{}
for _, s := range servers {
mid := s.Machine
if _, ok := unique[mid]; !ok {
unique[mid] = &api.Server{Addr: s.Addr, PingURL: s.PingURL, Id: s.Id, InGroup: true}
}
v := unique[mid]
if v != nil {
v.Replicas++
}
}
for _, v := range unique {
response.Servers = append(response.Servers, *v)
}
}
if len(response.Servers) > 0 {
sort.SliceStable(response.Servers, func(i, j int) bool {
if response.Servers[i].Addr != response.Servers[j].Addr {
return response.Servers[i].Addr < response.Servers[j].Addr
}
return response.Servers[i].Port < response.Servers[j].Port
})
}
u.Notify(api.GetWorkerList, response)
}

View file

@ -1,63 +1,191 @@
package coordinator
import (
"errors"
"fmt"
"log"
"sync"
"sync/atomic"
"github.com/giongto35/cloud-game/v2/pkg/cws"
"github.com/gorilla/websocket"
"github.com/giongto35/cloud-game/v3/pkg/api"
"github.com/giongto35/cloud-game/v3/pkg/com"
"github.com/giongto35/cloud-game/v3/pkg/logger"
)
type WorkerClient struct {
*cws.Client
type Worker struct {
AppLibrary
Connection
RegionalClient
Session
slotted
WorkerID string
Address string // ip address of worker
// public server used for ping check
PingServer string
StunTurnServer string
userCount int // may be atomic
Zone string
Addr string
PingServer string
Port string
RoomId string // room reference
Tag string
Zone string
mu sync.Mutex
Lib []api.GameInfo
Sessions map[string]struct{}
log *logger.Logger
}
// NewWorkerClient returns a client connecting to worker.
// This connection exchanges information between workers and server.
func NewWorkerClient(c *websocket.Conn, workerID string) *WorkerClient {
return &WorkerClient{
Client: cws.NewClient(c),
WorkerID: workerID,
type RegionalClient interface {
In(region string) bool
}
type HasUserRegistry interface {
Find(id string) *User
}
type AppLibrary interface {
SetLib([]api.GameInfo)
AppNames() []api.GameInfo
}
type Session interface {
AddSession(id string)
// HadSession is true when an old session is found
HadSession(id string) bool
SetSessions(map[string]struct{})
}
type AppMeta struct {
Alias string
Base string
Name string
Path string
System string
Type string
}
func NewWorker(sock *com.Connection, handshake api.ConnectionRequest[com.Uid], log *logger.Logger) *Worker {
conn := com.NewConnection[api.PT, api.In[com.Uid], api.Out, *api.Out](sock, handshake.Id, log)
return &Worker{
Connection: conn,
Addr: handshake.Addr,
PingServer: handshake.PingURL,
Port: handshake.Port,
Tag: handshake.Tag,
Zone: handshake.Zone,
log: log.Extend(log.With().
Str(logger.ClientField, logger.MarkNone).
Str(logger.DirectionField, logger.MarkNone).
Str("cid", conn.Id().Short())),
}
}
// ChangeUserQuantityBy increases or decreases the total amount of
// users connected to the current worker.
// We count users to determine when the worker becomes new game ready.
func (wc *WorkerClient) ChangeUserQuantityBy(n int) {
wc.mu.Lock()
wc.userCount += n
// just to be on a safe side
if wc.userCount < 0 {
wc.userCount = 0
func (w *Worker) HandleRequests(users HasUserRegistry) chan struct{} {
return w.ProcessPackets(func(p api.In[com.Uid]) (err error) {
switch p.T {
case api.RegisterRoom:
err = api.Do(p, func(d api.RegisterRoomRequest) {
w.log.Info().Msgf("set room [%v] = %v", w.Id(), d)
w.HandleRegisterRoom(d)
})
case api.CloseRoom:
err = api.Do(p, w.HandleCloseRoom)
case api.IceCandidate:
err = api.DoE(p, func(d api.WebrtcIceCandidateRequest) error {
return w.HandleIceCandidate(d, users)
})
case api.LibNewGameList:
err = api.DoE(p, w.HandleLibGameList)
case api.PrevSessions:
err = api.DoE(p, w.HandlePrevSessionList)
default:
w.log.Warn().Msgf("Unknown packet: %+v", p)
}
if err != nil && !errors.Is(err, api.ErrMalformed) {
w.log.Error().Err(err).Send()
err = api.ErrMalformed
}
return
})
}
func (w *Worker) SetLib(list []api.GameInfo) { w.Lib = list }
func (w *Worker) AppNames() []api.GameInfo {
return w.Lib
}
func (w *Worker) AddSession(id string) {
// sessions can be uninitialized until the coordinator pushes them to the worker
if w.Sessions == nil {
return
}
wc.mu.Unlock()
w.Sessions[id] = struct{}{}
}
// HasGameSlot tells whether the current worker has a
// free slot to start a new game.
// Workers support only one game at a time.
func (wc *WorkerClient) HasGameSlot() bool {
wc.mu.Lock()
defer wc.mu.Unlock()
return wc.userCount == 0
func (w *Worker) HadSession(id string) bool {
_, ok := w.Sessions[id]
return ok
}
func (wc *WorkerClient) Printf(format string, args ...interface{}) {
log.Printf(fmt.Sprintf("Worker %s] %s", wc.WorkerID, format), args...)
func (w *Worker) SetSessions(sessions map[string]struct{}) {
w.Sessions = sessions
}
func (wc *WorkerClient) Println(args ...interface{}) {
log.Println(fmt.Sprintf("Worker %s] %s", wc.WorkerID, fmt.Sprint(args...)))
// In say whether some worker from this region (zone).
// Empty region always returns true.
func (w *Worker) In(region string) bool { return region == "" || region == w.Zone }
// slotted used for tracking user slots and the availability.
type slotted int32
// HasSlot checks if the current worker has a free slot to start a new game.
// Workers support only one game at a time, so it returns true in case if
// there are no players in the room (worker).
func (s *slotted) HasSlot() bool { return atomic.LoadInt32((*int32)(s)) == 0 }
// TryReserve reserves the slot only when it's free.
func (s *slotted) TryReserve() bool {
for {
current := atomic.LoadInt32((*int32)(s))
if current != 0 {
return false
}
if atomic.CompareAndSwapInt32((*int32)(s), 0, 1) {
return true
}
}
}
// UnReserve decrements user counter of the worker.
func (s *slotted) UnReserve() {
for {
current := atomic.LoadInt32((*int32)(s))
if current <= 0 {
// reset to zero
if current < 0 {
if atomic.CompareAndSwapInt32((*int32)(s), current, 0) {
return
}
continue
}
return
}
// Regular decrement for positive values
newVal := current - 1
if atomic.CompareAndSwapInt32((*int32)(s), current, newVal) {
return
}
}
}
func (s *slotted) FreeSlots() { atomic.StoreInt32((*int32)(s), 0) }
func (w *Worker) Disconnect() {
w.Connection.Disconnect()
w.RoomId = ""
w.FreeSlots()
}
func (w *Worker) PrintInfo() string {
return fmt.Sprintf("id: %v, addr: %v, port: %v, zone: %v, ping addr: %v, tag: %v",
w.Id(), w.Addr, w.Port, w.Zone, w.PingServer, w.Tag)
}

View file

@ -0,0 +1,193 @@
package coordinator
import (
"sync"
"sync/atomic"
"testing"
)
func TestSlotted(t *testing.T) {
t.Run("UnReserve", func(t *testing.T) {
t.Run("BasicDecrement", testUnReserveBasic)
t.Run("PreventUnderflow", testUnReserveUnderflow)
t.Run("ConcurrentDecrement", testUnReserveConcurrent)
})
t.Run("TryReserve", func(t *testing.T) {
t.Run("SuccessWhenZero", testTryReserveSuccess)
t.Run("FailWhenNonZero", testTryReserveFailure)
t.Run("ConcurrentReservations", testTryReserveConcurrent)
})
t.Run("Integration", func(t *testing.T) {
t.Run("ReserveUnreserveFlow", testReserveUnreserveFlow)
t.Run("FreeSlots", testFreeSlots)
t.Run("HasSlot", testHasSlot)
})
}
func testUnReserveBasic(t *testing.T) {
t.Parallel()
var s slotted
// Initial state
if atomic.LoadInt32((*int32)(&s)) != 0 {
t.Fatal("initial state not zero")
}
// Test normal decrement
s.TryReserve() // 0 -> 1
s.UnReserve()
if atomic.LoadInt32((*int32)(&s)) != 0 {
t.Error("failed to decrement to zero")
}
// Test multiple decrements
s.TryReserve() // 0 -> 1
s.TryReserve() // 1 -> 2
s.UnReserve()
s.UnReserve()
if atomic.LoadInt32((*int32)(&s)) != 0 {
t.Error("failed to decrement multiple times")
}
}
func testUnReserveUnderflow(t *testing.T) {
t.Parallel()
var s slotted
t.Run("PreventNewUnderflow", func(t *testing.T) {
s.UnReserve() // Start at 0
if atomic.LoadInt32((*int32)(&s)) != 0 {
t.Error("should remain at 0 when unreserving from 0")
}
})
t.Run("FixExistingNegative", func(t *testing.T) {
atomic.StoreInt32((*int32)(&s), -5)
s.UnReserve()
if current := atomic.LoadInt32((*int32)(&s)); current != 0 {
t.Errorf("should fix negative value to 0, got %d", current)
}
})
}
func testUnReserveConcurrent(t *testing.T) {
t.Parallel()
var s slotted
const workers = 100
var wg sync.WaitGroup
atomic.StoreInt32((*int32)(&s), int32(workers))
wg.Add(workers)
for range workers {
go func() {
defer wg.Done()
s.UnReserve()
}()
}
wg.Wait()
if current := atomic.LoadInt32((*int32)(&s)); current != 0 {
t.Errorf("unexpected final value: %d (want 0)", current)
}
}
func testTryReserveSuccess(t *testing.T) {
t.Parallel()
var s slotted
if !s.TryReserve() {
t.Error("should succeed when zero")
}
if atomic.LoadInt32((*int32)(&s)) != 1 {
t.Error("failed to increment")
}
}
func testTryReserveFailure(t *testing.T) {
t.Parallel()
var s slotted
atomic.StoreInt32((*int32)(&s), 1)
if s.TryReserve() {
t.Error("should fail when non-zero")
}
}
func testTryReserveConcurrent(t *testing.T) {
t.Parallel()
var s slotted
const workers = 100
var success int32
var wg sync.WaitGroup
wg.Add(workers)
for range workers {
go func() {
defer wg.Done()
if s.TryReserve() {
atomic.AddInt32(&success, 1)
}
}()
}
wg.Wait()
if success != 1 {
t.Errorf("unexpected success count: %d (want 1)", success)
}
if atomic.LoadInt32((*int32)(&s)) != 1 {
t.Error("counter not properly incremented")
}
}
func testReserveUnreserveFlow(t *testing.T) {
t.Parallel()
var s slotted
// Successful reservation
if !s.TryReserve() {
t.Fatal("failed initial reservation")
}
// Second reservation should fail
if s.TryReserve() {
t.Error("unexpected successful second reservation")
}
// Unreserve and try again
s.UnReserve()
if !s.TryReserve() {
t.Error("failed reservation after unreserve")
}
}
func testFreeSlots(t *testing.T) {
t.Parallel()
var s slotted
// Set to arbitrary value
atomic.StoreInt32((*int32)(&s), 5)
s.FreeSlots()
if atomic.LoadInt32((*int32)(&s)) != 0 {
t.Error("FreeSlots failed to reset counter")
}
}
func testHasSlot(t *testing.T) {
t.Parallel()
var s slotted
if !s.HasSlot() {
t.Error("should have slot when zero")
}
s.TryReserve()
if s.HasSlot() {
t.Error("shouldn't have slot when reserved")
}
}

View file

@ -0,0 +1,68 @@
package coordinator
import "github.com/giongto35/cloud-game/v3/pkg/api"
func (w *Worker) WebrtcInit(id string) (*api.WebrtcInitResponse, error) {
return api.UnwrapChecked[api.WebrtcInitResponse](
w.Send(api.WebrtcInit, api.WebrtcInitRequest{Id: id}))
}
func (w *Worker) WebrtcAnswer(id string, sdp string) {
w.Notify(api.WebrtcAnswer,
api.WebrtcAnswerRequest{Stateful: api.Stateful{Id: id}, Sdp: sdp})
}
func (w *Worker) WebrtcIceCandidate(id string, candidate string) {
w.Notify(api.WebrtcIce,
api.WebrtcIceCandidateRequest{Stateful: api.Stateful{Id: id}, Candidate: candidate})
}
func (w *Worker) StartGame(id string, req api.GameStartUserRequest) (*api.StartGameResponse, error) {
return api.UnwrapChecked[api.StartGameResponse](
w.Send(api.StartGame, api.StartGameRequest{
StatefulRoom: api.StatefulRoom{Id: id, Rid: req.RoomId},
Game: req.GameName,
PlayerIndex: req.PlayerIndex,
Record: req.Record,
RecordUser: req.RecordUser,
}))
}
func (w *Worker) QuitGame(id string) {
w.Notify(api.QuitGame, api.GameQuitRequest{Id: id, Rid: w.RoomId})
}
func (w *Worker) SaveGame(id string) (*api.SaveGameResponse, error) {
return api.UnwrapChecked[api.SaveGameResponse](
w.Send(api.SaveGame, api.SaveGameRequest{Id: id, Rid: w.RoomId}))
}
func (w *Worker) LoadGame(id string) (*api.LoadGameResponse, error) {
return api.UnwrapChecked[api.LoadGameResponse](
w.Send(api.LoadGame, api.LoadGameRequest{Id: id, Rid: w.RoomId}))
}
func (w *Worker) ChangePlayer(id string, index int) (*api.ChangePlayerResponse, error) {
return api.UnwrapChecked[api.ChangePlayerResponse](
w.Send(api.ChangePlayer, api.ChangePlayerRequest{
StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
Index: index,
}))
}
func (w *Worker) ResetGame(id string) {
w.Notify(api.ResetGame, api.ResetGameRequest{Id: id, Rid: w.RoomId})
}
func (w *Worker) RecordGame(id string, rec bool, recUser string) (*api.RecordGameResponse, error) {
return api.UnwrapChecked[api.RecordGameResponse](
w.Send(api.RecordGame, api.RecordGameRequest{
StatefulRoom: api.StatefulRoom{Id: id, Rid: w.RoomId},
Active: rec,
User: recUser,
}))
}
func (w *Worker) TerminateSession(id string) {
_, _ = w.Send(api.TerminateSession, api.TerminateSessionRequest{Id: id})
}

View file

@ -0,0 +1,39 @@
package coordinator
import "github.com/giongto35/cloud-game/v3/pkg/api"
func (w *Worker) HandleRegisterRoom(rq api.RegisterRoomRequest) { w.RoomId = string(rq) }
func (w *Worker) HandleCloseRoom(rq api.CloseRoomRequest) {
if string(rq) == w.RoomId {
w.RoomId = ""
w.FreeSlots()
}
}
func (w *Worker) HandleIceCandidate(rq api.WebrtcIceCandidateRequest, users HasUserRegistry) error {
if usr := users.Find(rq.Id); usr != nil {
usr.SendWebrtcIceCandidate(rq.Candidate)
} else {
w.log.Warn().Str("id", rq.Id).Msg("unknown session")
}
return nil
}
func (w *Worker) HandleLibGameList(inf api.LibGameListInfo) error {
w.SetLib(inf.List)
return nil
}
func (w *Worker) HandlePrevSessionList(sess api.PrevSessionInfo) error {
if len(sess.List) == 0 {
return nil
}
m := make(map[string]struct{})
for _, v := range sess.List {
m[v] = struct{}{}
}
w.SetSessions(m)
return nil
}

View file

@ -1,23 +0,0 @@
package api
import "encoding/json"
// This list of postfixes is used in the API:
// - *Request postfix denotes clients calls (i.e. from a browser to the HTTP-server).
// - *Call postfix denotes IPC calls (from the coordinator to a worker).
func from(source interface{}, data string) error {
err := json.Unmarshal([]byte(data), source)
if err != nil {
return err
}
return nil
}
func to(target interface{}) (string, error) {
b, err := json.Marshal(target)
if err != nil {
return "", err
}
return string(b), nil
}

View file

@ -1,54 +0,0 @@
package api
import "github.com/giongto35/cloud-game/v2/pkg/cws"
const (
GetRoom = "get_room"
CloseRoom = "close_room"
RegisterRoom = "register_room"
Heartbeat = "heartbeat"
IceCandidate = "ice_candidate"
NoData = ""
InitWebrtc = "init_webrtc"
Answer = "answer"
GameStart = "start"
GameQuit = "quit"
GameSave = "save"
GameLoad = "load"
GamePlayerSelect = "player_index"
GameMultitap = "multitap"
)
type GameStartRequest struct {
GameName string `json:"game_name"`
}
func (packet *GameStartRequest) From(data string) error { return from(packet, data) }
type GameStartCall struct {
Name string `json:"name"`
Base string `json:"base"`
Path string `json:"path"`
Type string `json:"type"`
}
func (packet *GameStartCall) From(data string) error { return from(packet, data) }
func (packet *GameStartCall) To() (string, error) { return to(packet) }
type ConnectionRequest struct {
Zone string `json:"zone,omitempty"`
PingAddr string `json:"ping_addr,omitempty"`
IsHTTPS bool `json:"is_https,omitempty"`
}
// packets
func RegisterRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: RegisterRoom, Data: data} }
func GetRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: GetRoom, Data: data} }
func CloseRoomPacket(data string) cws.WSPacket { return cws.WSPacket{ID: CloseRoom, Data: data} }
func IceCandidatePacket(data string, sessionId string) cws.WSPacket {
return cws.WSPacket{ID: IceCandidate, Data: data, SessionID: sessionId}
}

View file

@ -1,21 +0,0 @@
package api
import "github.com/giongto35/cloud-game/v2/pkg/cws"
const (
ServerId = "server_id"
TerminateSession = "terminateSession"
)
type ConfPushCall struct {
Data []byte `json:"data"`
}
func (packet *ConfPushCall) From(data string) error { return from(packet, data) }
func (packet *ConfPushCall) To() (string, error) { return to(packet) }
func ServerIdPacket(id string) cws.WSPacket { return cws.WSPacket{ID: ServerId, Data: id} }
func ConfigRequestPacket(conf []byte) cws.WSPacket { return cws.WSPacket{Data: string(conf)} }
func TerminateSessionPacket(sessionId string) cws.WSPacket {
return cws.WSPacket{ID: TerminateSession, SessionID: sessionId}
}

View file

@ -1,221 +0,0 @@
package cws
import (
"encoding/json"
"log"
"runtime/debug"
"sync"
"time"
"github.com/gofrs/uuid"
"github.com/gorilla/websocket"
)
type (
Client struct {
id string
conn *websocket.Conn
sendLock sync.Mutex
// sendCallback is callback based on packetID
sendCallback map[string]func(req WSPacket)
sendCallbackLock sync.Mutex
// recvCallback is callback when receive based on ID of the packet
recvCallback map[string]func(req WSPacket)
Done chan struct{}
}
WSPacket struct {
ID string `json:"id"`
// TODO: Make Data generic: map[string]interface{} for more usecases
Data string `json:"data"`
RoomID string `json:"room_id"`
PlayerIndex int `json:"player_index"`
PacketID string `json:"packet_id"`
// Globally ID of a browser session
SessionID string `json:"session_id"`
}
PacketHandler func(resp WSPacket) (req WSPacket)
)
var (
EmptyPacket = WSPacket{}
HeartbeatPacket = WSPacket{ID: "heartbeat"}
)
const WSWait = 20 * time.Second
func NewClient(conn *websocket.Conn) *Client {
id := uuid.Must(uuid.NewV4()).String()
sendCallback := map[string]func(WSPacket){}
recvCallback := map[string]func(WSPacket){}
return &Client{
id: id,
conn: conn,
sendCallback: sendCallback,
recvCallback: recvCallback,
Done: make(chan struct{}),
}
}
// Send sends a packet and trigger callback when the packet comes back
func (c *Client) Send(request WSPacket, callback func(response WSPacket)) {
request.PacketID = uuid.Must(uuid.NewV4()).String()
data, err := json.Marshal(request)
if err != nil {
return
}
// TODO: Consider using lock free
// Wrap callback with sessionID and packetID
if callback != nil {
wrapperCallback := func(resp WSPacket) {
defer func() {
if err := recover(); err != nil {
log.Println("Recovered from err in client callback ", err)
}
}()
resp.PacketID = request.PacketID
resp.SessionID = request.SessionID
callback(resp)
}
c.sendCallbackLock.Lock()
c.sendCallback[request.PacketID] = wrapperCallback
c.sendCallbackLock.Unlock()
}
c.sendLock.Lock()
c.conn.SetWriteDeadline(time.Now().Add(WSWait))
c.conn.WriteMessage(websocket.TextMessage, data)
c.sendLock.Unlock()
}
// Receive receive and response back
func (c *Client) Receive(id string, f PacketHandler) {
c.recvCallback[id] = func(response WSPacket) {
defer func() {
if err := recover(); err != nil {
log.Println("Recovered from err ", err)
log.Println(debug.Stack())
}
}()
req := f(response)
// Add Meta data
req.PacketID = response.PacketID
req.SessionID = response.SessionID
// Skip response if it is EmptyPacket
if response == EmptyPacket {
return
}
resp, err := json.Marshal(req)
if err != nil {
log.Println("[!] json marshal error:", err)
}
c.sendLock.Lock()
c.conn.SetWriteDeadline(time.Now().Add(WSWait))
c.conn.WriteMessage(websocket.TextMessage, resp)
c.sendLock.Unlock()
}
}
// SyncSend sends a packet and wait for callback till the packet comes back
func (c *Client) SyncSend(request WSPacket) (response WSPacket) {
res := make(chan WSPacket)
f := func(resp WSPacket) {
res <- resp
}
c.Send(request, f)
return <-res
}
// SendAwait sends some packet while waiting for a tile-limited response
//func (c *Client) SendAwait(packet WSPacket) WSPacket {
// ch := make(chan WSPacket)
// defer close(ch)
// c.Send(packet, func(response WSPacket) { ch <- response })
//
// for {
// select {
// case packet := <-ch:
// return packet
// case <-time.After(config.WsIpcTimeout):
// log.Printf("Packet receive timeout!")
// return EmptyPacket
// }
// }
//}
// Heartbeat maintains connection to coordinator.
// Blocking.
func (c *Client) Heartbeat() {
// send heartbeat every 1s
t := time.NewTicker(time.Second)
// don't wait 1 second
c.Send(HeartbeatPacket, nil)
for {
select {
case <-c.Done:
t.Stop()
log.Printf("Close heartbeat")
return
case <-t.C:
c.Send(HeartbeatPacket, nil)
}
}
}
func (c *Client) Listen() {
for {
c.conn.SetReadDeadline(time.Now().Add(WSWait))
_, rawMsg, err := c.conn.ReadMessage()
if err != nil {
log.Println("[!] read:", err)
// TODO: Check explicit disconnect error to break
close(c.Done)
break
}
wspacket := WSPacket{}
err = json.Unmarshal(rawMsg, &wspacket)
if err != nil {
log.Println("Warn: error decoding", rawMsg)
continue
}
// Check if some async send is waiting for the response based on packetID
// TODO: Change to read lock.
//c.sendCallbackLock.Lock()
callback, ok := c.sendCallback[wspacket.PacketID]
//c.sendCallbackLock.Unlock()
if ok {
go callback(wspacket)
//c.sendCallbackLock.Lock()
delete(c.sendCallback, wspacket.PacketID)
//c.sendCallbackLock.Unlock()
// Skip receiveCallback to avoid duplication
continue
}
// Check if some receiver with the ID is registered
if callback, ok := c.recvCallback[wspacket.ID]; ok {
go callback(wspacket)
}
}
}
func (c *Client) Close() {
if c == nil || c.conn == nil {
return
}
c.conn.Close()
}

View file

@ -1,10 +0,0 @@
package backend
type Download struct {
Key string
Address string
}
type Client interface {
Request(dest string, urls ...Download) ([]string, []string)
}

View file

@ -1,58 +0,0 @@
package backend
import (
"crypto/tls"
"log"
"net/http"
"github.com/cavaliercoder/grab"
)
type GrabDownloader struct {
client *grab.Client
concurrency int
}
func NewGrabDownloader() GrabDownloader {
client := grab.Client{
UserAgent: "Cloud-Game/2.2",
HTTPClient: &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
},
}
return GrabDownloader{
client: &client,
concurrency: 5,
}
}
func (d GrabDownloader) Request(dest string, urls ...Download) (ok []string, nook []string) {
reqs := make([]*grab.Request, 0)
for _, url := range urls {
req, err := grab.NewRequest(dest, url.Address)
if err != nil {
log.Printf("error: couldn't make request URL: %v, %v", url, err)
} else {
req.Label = url.Key
reqs = append(reqs, req)
}
}
// check each response
for resp := range d.client.DoBatch(d.concurrency, reqs...) {
r := resp.Request
if err := resp.Err(); err != nil {
log.Printf("error: download [%s] %s failed: %v\n", r.Label, r.URL(), err)
if resp.HTTPResponse.StatusCode == 404 {
nook = append(nook, resp.Request.Label)
}
} else {
log.Printf("Downloaded [%v] [%s] -> %s\n", resp.HTTPResponse.Status, r.Label, resp.Filename)
ok = append(ok, resp.Filename)
}
}
return
}

View file

@ -1,39 +0,0 @@
package downloader
import (
"github.com/giongto35/cloud-game/v2/pkg/downloader/backend"
"github.com/giongto35/cloud-game/v2/pkg/downloader/pipe"
)
type Downloader struct {
backend backend.Client
// pipe contains a sequential list of
// operations applied to some files and
// each operation will return a list of
// successfully processed files
pipe []Process
}
type Process func(string, []string) []string
func NewDefaultDownloader() Downloader {
return Downloader{
backend: backend.NewGrabDownloader(),
pipe: []Process{
pipe.Unpack,
pipe.Delete,
}}
}
// Download tries to download specified with URLs list of files and
// put them into the destination folder.
// It will return a partial or full list of downloaded files,
// a list of processed files if some pipe processing functions are set.
func (d *Downloader) Download(dest string, urls ...backend.Download) ([]string, []string) {
files, fails := d.backend.Request(dest, urls...)
for _, op := range d.pipe {
files = op(dest, files)
}
return files, fails
}

Some files were not shown because too many files have changed in this diff Show more