mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-07-21 00:59:22 +00:00
merge master
This commit is contained in:
commit
79b0337ee8
20 changed files with 924 additions and 384 deletions
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
* linguist-vendored
|
||||
*.go linguist-vendored=false
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -45,3 +45,6 @@ Network Trash Folder
|
|||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### Production
|
||||
DockerfileProd
|
||||
prod/
|
||||
|
|
|
|||
1
Dockerfile
vendored
1
Dockerfile
vendored
|
|
@ -13,6 +13,7 @@ RUN apt-get update
|
|||
RUN apt-get install pkg-config libvpx-dev libopus-dev libopusfile-dev -y
|
||||
RUN go get github.com/pion/webrtc
|
||||
RUN go get github.com/gorilla/websocket
|
||||
RUN go get github.com/satori/go.uuid
|
||||
RUN go install github.com/giongto35/cloud-game
|
||||
|
||||
EXPOSE 8000
|
||||
|
|
|
|||
10
README.md
vendored
10
README.md
vendored
|
|
@ -1,8 +1,8 @@
|
|||
# Cloud Gaming Service Lite
|
||||
|
||||
Cloud Gaming Service is an open source Cloud Gaming Service building on [WebRTC](https://github.com/pion).
|
||||
Cloud Gaming Service Lite is an open source Cloud Gaming Service building on [WebRTC](https://github.com/pion).
|
||||
|
||||
With cloud gaming, you can play any of your favourite NES game directly on your browser without installing it. It also brings modern online multiplayer gaming experience to classic NES game, so two people can play the game together.
|
||||
With cloud gaming, you can play any of your favourite NES games directly on your browser without installing it. It also brings modern online multiplayer gaming experience to classic NES game, so two people can play the game together.
|
||||
|
||||

|
||||
|
||||
|
|
@ -34,13 +34,15 @@ US East: (Haven't hosted)
|
|||
|
||||
Europe: (Haven't hosted)
|
||||
* [http://eu.nes.webgame2d.com](http://eu.nes.webgame2d.com)
|
||||
* [http://eu.nes.playcloud.games](http://eu.nes.playcloud.games)
|
||||
* [http://eu.nes.playcloud.games](http://eu.nes.playcloud.games)
|
||||
|
||||
Note: The current state of cloud gaming service lite are not optimized for production. The service will still experience lag in the case of heavy traffic. You can try hosting your own service following the instruction in the next session.
|
||||
|
||||
## Run on local
|
||||
|
||||
You can host the server yourself by running `./run_local.sh`. It will spawn a docker environment and you can access the emulator on `localhost:8000`.
|
||||
|
||||
You can open port, so other person can access your local machine and play the game together.
|
||||
You can open port on your machine, so other person can access your local machine and play the game together.
|
||||
|
||||
## Development environment
|
||||
|
||||
|
|
|
|||
10
config/config.go
Normal file
10
config/config.go
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
package config
|
||||
|
||||
import "flag"
|
||||
|
||||
const defaultoverlord = "ws://localhost:9000/wso"
|
||||
|
||||
var IsDebug = flag.Bool("debug", false, "Is game running in debug mode?")
|
||||
var OverlordHost = flag.String("overlordhost", defaultoverlord, "Specify the path for overlord. If the flag is `overlord`, the server will be run as overlord")
|
||||
var Port = flag.String("port", "8000", "Port of the game")
|
||||
var IsMonitor = flag.Bool("monitor", false, "Turn on monitor")
|
||||
609
main.go
609
main.go
|
|
@ -1,9 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"flag"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
|
|
@ -11,80 +10,104 @@ import (
|
|||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/config"
|
||||
"github.com/giongto35/cloud-game/ui"
|
||||
"github.com/giongto35/cloud-game/util"
|
||||
"github.com/giongto35/cloud-game/webrtc"
|
||||
"github.com/gorilla/websocket"
|
||||
pionRTC "github.com/pion/webrtc"
|
||||
|
||||
"gopkg.in/hraban/opus.v2"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
|
||||
const (
|
||||
width = 256
|
||||
height = 240
|
||||
scale = 3
|
||||
title = "NES"
|
||||
width = 256
|
||||
height = 240
|
||||
scale = 3
|
||||
title = "NES"
|
||||
gameboyIndex = "./static/gameboy.html"
|
||||
debugIndex = "./static/index_ws.html"
|
||||
debugIndex = "./static/index_ws.html"
|
||||
)
|
||||
|
||||
|
||||
var indexFN = gameboyIndex
|
||||
|
||||
// Time allowed to write a message to the peer.
|
||||
var readWait = 30 * time.Second
|
||||
var writeWait = 30 * time.Second
|
||||
|
||||
var IsOverlord = false
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
type WSPacket struct {
|
||||
ID string `json:"id"`
|
||||
Data string `json:"data"`
|
||||
RoomID string `json:"room_id"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
|
||||
// Room is a game session. multi webRTC sessions can connect to a same game.
|
||||
// A room stores all the channel for interaction between all webRTCs session and emulator
|
||||
type Room struct {
|
||||
imageChannel chan *image.RGBA
|
||||
audioChanel chan float32
|
||||
audioChannel chan float32
|
||||
inputChannel chan int
|
||||
// closedChannel is to fire exit event when there is no webRTC session running
|
||||
closedChannel chan bool
|
||||
// Done channel is to fire exit event when there is no webRTC session running
|
||||
Done chan struct{}
|
||||
|
||||
rtcSessions []*webrtc.WebRTC
|
||||
rtcSessions []*webrtc.WebRTC
|
||||
sessionsLock *sync.Mutex
|
||||
|
||||
director *ui.Director
|
||||
}
|
||||
|
||||
var rooms map[string]*Room
|
||||
var rooms = map[string]*Room{}
|
||||
|
||||
// ID to peerconnection
|
||||
var peerconnections = map[string]*webrtc.WebRTC{}
|
||||
var serverID = ""
|
||||
var oclient *Client
|
||||
|
||||
func main() {
|
||||
fmt.Println("Usage: ./game [debug]")
|
||||
if len(os.Args) > 1 {
|
||||
flag.Parse()
|
||||
log.Println("Usage: ./game [debug]")
|
||||
if *config.IsDebug {
|
||||
// debug
|
||||
indexFN = debugIndex
|
||||
fmt.Println("Use debug version")
|
||||
log.Println("Use debug version")
|
||||
}
|
||||
|
||||
if *config.OverlordHost == "overlord" {
|
||||
log.Println("Running as overlord ")
|
||||
IsOverlord = true
|
||||
} else {
|
||||
if strings.HasPrefix(*config.OverlordHost, "ws") && !strings.HasSuffix(*config.OverlordHost, "wso") {
|
||||
log.Fatal("Overlord connection is invalid. Should have the form `ws://.../wso`")
|
||||
}
|
||||
log.Println("Running as slave ")
|
||||
IsOverlord = false
|
||||
}
|
||||
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
fmt.Println("http://localhost:8000")
|
||||
rooms = map[string]*Room{}
|
||||
|
||||
// ignore origin
|
||||
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
|
||||
http.HandleFunc("/ws", ws)
|
||||
|
||||
http.HandleFunc("/", getWeb)
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
||||
http.ListenAndServe(":8000", nil)
|
||||
}
|
||||
http.HandleFunc("/ws", ws)
|
||||
|
||||
if !IsOverlord {
|
||||
oclient = NewOverlordClient()
|
||||
}
|
||||
|
||||
log.Println("oclient ", oclient)
|
||||
if !IsOverlord {
|
||||
log.Println("http://localhost:" + *config.Port)
|
||||
http.ListenAndServe(":"+*config.Port, nil)
|
||||
} else {
|
||||
log.Println("http://localhost:9000")
|
||||
// Overlord expose one more path for handle overlord connections
|
||||
http.HandleFunc("/wso", wso)
|
||||
http.ListenAndServe(":9000", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func getWeb(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := ioutil.ReadFile(indexFN)
|
||||
|
|
@ -100,25 +123,26 @@ func initRoom(roomID, gameName string) string {
|
|||
if roomID == "" {
|
||||
roomID = generateRoomID()
|
||||
}
|
||||
log.Println("Init new room", roomID)
|
||||
imageChannel := make(chan *image.RGBA, 100)
|
||||
audioChannel := make(chan float32, 48000)
|
||||
audioChannel := make(chan float32, ui.SampleRate)
|
||||
inputChannel := make(chan int, 100)
|
||||
closedChannel := make(chan bool)
|
||||
|
||||
// create director
|
||||
director := ui.NewDirector(roomID, imageChannel, audioChannel, inputChannel, closedChannel)
|
||||
|
||||
rooms[roomID] = &Room{
|
||||
imageChannel: imageChannel,
|
||||
audioChanel: audioChannel,
|
||||
inputChannel: inputChannel,
|
||||
closedChannel: closedChannel,
|
||||
rtcSessions: []*webrtc.WebRTC{},
|
||||
director: director,
|
||||
}
|
||||
director := ui.NewDirector(roomID, imageChannel, audioChannel, inputChannel)
|
||||
|
||||
go fanoutScreen(imageChannel, roomID)
|
||||
go fanoutAudio(audioChannel, roomID)
|
||||
room := &Room{
|
||||
imageChannel: imageChannel,
|
||||
audioChannel: audioChannel,
|
||||
inputChannel: inputChannel,
|
||||
rtcSessions: []*webrtc.WebRTC{},
|
||||
sessionsLock: &sync.Mutex{},
|
||||
director: director,
|
||||
Done: make(chan struct{}),
|
||||
}
|
||||
rooms[roomID] = room
|
||||
|
||||
go room.start()
|
||||
go director.Start([]string{"games/" + gameName})
|
||||
|
||||
return roomID
|
||||
|
|
@ -142,21 +166,35 @@ func isRoomRunning(roomID string) bool {
|
|||
}
|
||||
|
||||
// startSession handles one session call
|
||||
func startSession(webRTC *webrtc.WebRTC, gameName string, roomID string, playerIndex int) string {
|
||||
func startSession(webRTC *webrtc.WebRTC, gameName string, roomID string, playerIndex int) (rRoomID string, isNewRoom bool) {
|
||||
isNewRoom = false
|
||||
cleanSession(webRTC)
|
||||
// If the roomID is empty,
|
||||
// or the roomID doesn't have any running sessions (room was closed)
|
||||
// we spawn a new room
|
||||
if roomID == "" || !isRoomRunning(roomID) {
|
||||
roomID = initRoom(roomID, gameName)
|
||||
isNewRoom = true
|
||||
}
|
||||
|
||||
// TODO: Might have race condition
|
||||
rooms[roomID].rtcSessions = append(rooms[roomID].rtcSessions, webRTC)
|
||||
go faninInput(rooms[roomID].inputChannel, webRTC, playerIndex)
|
||||
room := rooms[roomID]
|
||||
|
||||
return roomID
|
||||
webRTC.AttachRoomID(roomID)
|
||||
go startWebRTCSession(room, webRTC, playerIndex)
|
||||
|
||||
return roomID, isNewRoom
|
||||
}
|
||||
|
||||
// Session represents a session connected from the browser to the current server
|
||||
type Session struct {
|
||||
client *Client
|
||||
peerconnection *webrtc.WebRTC
|
||||
ServerID string
|
||||
}
|
||||
|
||||
// Handle normal traffic (from browser to host)
|
||||
func ws(w http.ResponseWriter, r *http.Request) {
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
|
|
@ -164,223 +202,176 @@ func ws(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
log.Println("New ws connection")
|
||||
webRTC := webrtc.NewWebRTC()
|
||||
|
||||
// streaming game
|
||||
|
||||
// start new games and webrtc stuff?
|
||||
isDone := false
|
||||
|
||||
var gameName string
|
||||
var roomID string
|
||||
var playerIndex int
|
||||
|
||||
for !isDone {
|
||||
c.SetReadDeadline(time.Now().Add(readWait))
|
||||
mt, message, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("[!] read:", err)
|
||||
break
|
||||
}
|
||||
|
||||
req := WSPacket{}
|
||||
res := WSPacket{}
|
||||
|
||||
err = json.Unmarshal(message, &req)
|
||||
if err != nil {
|
||||
log.Println("[!] json unmarshal:", err)
|
||||
break
|
||||
}
|
||||
|
||||
// SDP connection initializations follows WebRTC convention
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols
|
||||
switch req.ID {
|
||||
case "ping":
|
||||
gameName = req.Data
|
||||
roomID = req.RoomID
|
||||
playerIndex = req.PlayerIndex
|
||||
log.Println("Ping from server with game:", gameName)
|
||||
res.ID = "pong"
|
||||
|
||||
case "sdp":
|
||||
log.Println("Received user SDP")
|
||||
localSession, err := webRTC.StartClient(req.Data, width, height)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
res.ID = "sdp"
|
||||
res.Data = localSession
|
||||
|
||||
case "candidate":
|
||||
// Unuse code
|
||||
hi := pionRTC.ICECandidateInit{}
|
||||
err = json.Unmarshal([]byte(req.Data), &hi)
|
||||
if err != nil {
|
||||
log.Println("[!] Cannot parse candidate: ", err)
|
||||
} else {
|
||||
// webRTC.AddCandidate(hi)
|
||||
}
|
||||
res.ID = "candidate"
|
||||
|
||||
case "start":
|
||||
log.Println("Starting game")
|
||||
roomID = startSession(webRTC, gameName, roomID, playerIndex)
|
||||
res.ID = "start"
|
||||
res.RoomID = roomID
|
||||
|
||||
// maybe we wont close websocket
|
||||
// isDone = true
|
||||
|
||||
case "save":
|
||||
log.Println("Saving game state")
|
||||
res.ID = "save"
|
||||
res.Data = "ok"
|
||||
if roomID != "" {
|
||||
err = rooms[roomID].director.SaveGame()
|
||||
if err != nil {
|
||||
log.Println("[!] Cannot save game state: ", err)
|
||||
res.Data = "error"
|
||||
}
|
||||
} else {
|
||||
res.Data = "error"
|
||||
}
|
||||
|
||||
case "load":
|
||||
log.Println("Loading game state")
|
||||
res.ID = "load"
|
||||
res.Data = "ok"
|
||||
if roomID != "" {
|
||||
err = rooms[roomID].director.LoadGame()
|
||||
if err != nil {
|
||||
log.Println("[!] Cannot load game state: ", err)
|
||||
res.Data = "error"
|
||||
}
|
||||
} else {
|
||||
res.Data = "error"
|
||||
}
|
||||
}
|
||||
|
||||
stRes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Println("json marshal:", err)
|
||||
}
|
||||
|
||||
c.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
err = c.WriteMessage(mt, []byte(stRes))
|
||||
if err != nil {
|
||||
log.Println("write:", err)
|
||||
break
|
||||
}
|
||||
// Create connection to overlord
|
||||
client := NewClient(c)
|
||||
//sessionID := strconv.Itoa(rand.Int())
|
||||
sessionID := uuid.Must(uuid.NewV4()).String()
|
||||
|
||||
wssession := &Session{
|
||||
client: client,
|
||||
peerconnection: webrtc.NewWebRTC(),
|
||||
// The server session is maintaining
|
||||
}
|
||||
|
||||
client.receive("heartbeat", func(resp WSPacket) WSPacket {
|
||||
return resp
|
||||
})
|
||||
|
||||
client.receive("initwebrtc", func(resp WSPacket) WSPacket {
|
||||
log.Println("Received user SDP")
|
||||
localSession, err := wssession.peerconnection.StartClient(resp.Data, width, height)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
return WSPacket{
|
||||
ID: "sdp",
|
||||
Data: localSession,
|
||||
SessionID: sessionID,
|
||||
}
|
||||
})
|
||||
|
||||
client.receive("save", func(resp WSPacket) (req WSPacket) {
|
||||
log.Println("Saving game state")
|
||||
req.ID = "save"
|
||||
req.Data = "ok"
|
||||
if roomID != "" {
|
||||
err = rooms[roomID].director.SaveGame()
|
||||
if err != nil {
|
||||
log.Println("[!] Cannot save game state: ", err)
|
||||
req.Data = "error"
|
||||
}
|
||||
} else {
|
||||
req.Data = "error"
|
||||
}
|
||||
|
||||
return req
|
||||
})
|
||||
|
||||
client.receive("load", func(resp WSPacket) (req WSPacket) {
|
||||
log.Println("Loading game state")
|
||||
req.ID = "load"
|
||||
req.Data = "ok"
|
||||
if roomID != "" {
|
||||
err = rooms[roomID].director.LoadGame()
|
||||
if err != nil {
|
||||
log.Println("[!] Cannot load game state: ", err)
|
||||
req.Data = "error"
|
||||
}
|
||||
} else {
|
||||
req.Data = "error"
|
||||
}
|
||||
|
||||
return req
|
||||
})
|
||||
|
||||
client.receive("start", func(resp WSPacket) (req WSPacket) {
|
||||
gameName = resp.Data
|
||||
roomID = resp.RoomID
|
||||
playerIndex = resp.PlayerIndex
|
||||
isNewRoom := false
|
||||
|
||||
log.Println("Starting game")
|
||||
// If we are connecting to overlord, request serverID from roomID
|
||||
if oclient != nil {
|
||||
roomServerID := getServerIDOfRoom(oclient, roomID)
|
||||
log.Println("Server of RoomID ", roomID, " is ", roomServerID)
|
||||
if roomServerID != "" && wssession.ServerID != roomServerID {
|
||||
// TODO: Re -register
|
||||
go bridgeConnection(wssession, roomServerID, gameName, roomID, playerIndex)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
roomID, isNewRoom = startSession(wssession.peerconnection, gameName, roomID, playerIndex)
|
||||
// Register room to overlord if we are connecting to overlord
|
||||
if isNewRoom && oclient != nil {
|
||||
oclient.send(WSPacket{
|
||||
ID: "registerRoom",
|
||||
Data: roomID,
|
||||
}, nil)
|
||||
}
|
||||
req.ID = "start"
|
||||
req.RoomID = roomID
|
||||
req.SessionID = sessionID
|
||||
|
||||
return req
|
||||
})
|
||||
|
||||
client.receive("candidate", func(resp WSPacket) (req WSPacket) {
|
||||
// Unuse code
|
||||
hi := pionRTC.ICECandidateInit{}
|
||||
err = json.Unmarshal([]byte(resp.Data), &hi)
|
||||
if err != nil {
|
||||
log.Println("[!] Cannot parse candidate: ", err)
|
||||
} else {
|
||||
// webRTC.AddCandidate(hi)
|
||||
}
|
||||
req.ID = "candidate"
|
||||
|
||||
return req
|
||||
})
|
||||
|
||||
client.listen()
|
||||
}
|
||||
|
||||
// generateRoomID generate a unique room ID containing 16 digits
|
||||
func generateRoomID() string {
|
||||
roomID := strconv.FormatInt(rand.Int63(), 16)
|
||||
//roomID := uuid.Must(uuid.NewV4()).String()
|
||||
return roomID
|
||||
}
|
||||
|
||||
// fanoutScreen fanout outputs to all webrtc in the same room
|
||||
func fanoutScreen(imageChannel chan *image.RGBA, roomID string) {
|
||||
for image := range imageChannel {
|
||||
isRoomRunning := false
|
||||
|
||||
yuv := util.RgbaToYuv(image)
|
||||
for _, webRTC := range rooms[roomID].rtcSessions {
|
||||
// Client stopped
|
||||
if webRTC.IsClosed() {
|
||||
continue
|
||||
}
|
||||
|
||||
// encode frame
|
||||
// fanout imageChannel
|
||||
if webRTC.IsConnected() {
|
||||
// NOTE: can block here
|
||||
webRTC.ImageChannel <- yuv
|
||||
}
|
||||
isRoomRunning = true
|
||||
}
|
||||
|
||||
if isRoomRunning == false {
|
||||
log.Println("Closed room from screen routine", roomID)
|
||||
rooms[roomID].closedChannel <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// fanoutAudio fanout outputs to all webrtc in the same room
|
||||
func fanoutAudio(audioChannel chan float32, roomID string) {
|
||||
log.Println("Enter fan audio")
|
||||
|
||||
enc, err := opus.NewEncoder(ui.SampleRate, ui.Channels, opus.AppAudio)
|
||||
|
||||
maxBufferSize := ui.TimeFrame * ui.SampleRate / 1000
|
||||
pcm := make([]float32, maxBufferSize) // 640 * 1000 / 16000 == 40 ms
|
||||
idx := 0
|
||||
|
||||
if err != nil {
|
||||
log.Println("[!] Cannot create audio encoder")
|
||||
return
|
||||
}
|
||||
|
||||
var count byte = 0
|
||||
|
||||
func (r *Room) start() {
|
||||
// fanout Screen
|
||||
for {
|
||||
pcm[idx] = <- audioChannel
|
||||
idx ++
|
||||
select {
|
||||
case <-r.Done:
|
||||
r.remove()
|
||||
return
|
||||
case image := <-r.imageChannel:
|
||||
//isRoomRunning := false
|
||||
|
||||
if idx == len(pcm) {
|
||||
data := make([]byte, 640)
|
||||
|
||||
n, err := enc.EncodeFloat32(pcm, data)
|
||||
|
||||
if err != nil {
|
||||
log.Println("[!] Failed to decode")
|
||||
continue
|
||||
}
|
||||
data = data[:n]
|
||||
// data = append(data, count)
|
||||
|
||||
|
||||
isRoomRunning := false
|
||||
for _, webRTC := range rooms[roomID].rtcSessions {
|
||||
yuv := util.RgbaToYuv(image)
|
||||
r.sessionsLock.Lock()
|
||||
for _, webRTC := range r.rtcSessions {
|
||||
// Client stopped
|
||||
if webRTC.IsClosed() {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// encode frame
|
||||
// fanout imageChannel
|
||||
if webRTC.IsConnected() {
|
||||
// NOTE: can block here
|
||||
webRTC.AudioChannel <- data
|
||||
webRTC.ImageChannel <- yuv
|
||||
}
|
||||
isRoomRunning = true
|
||||
//isRoomRunning = true
|
||||
}
|
||||
|
||||
if isRoomRunning == false {
|
||||
log.Println("Closed room from audio routine", roomID)
|
||||
rooms[roomID].closedChannel <- true
|
||||
return
|
||||
}
|
||||
idx = 0
|
||||
count = (count + 1) & 0xff
|
||||
r.sessionsLock.Unlock()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) remove() {
|
||||
log.Println("Closing room", r)
|
||||
r.director.Done <- struct{}{}
|
||||
}
|
||||
|
||||
|
||||
// faninInput fan-in of the same room to inputChannel
|
||||
func faninInput(inputChannel chan int, webRTC *webrtc.WebRTC, playerIndex int) {
|
||||
// startWebRTCSession fan-in of the same room to inputChannel
|
||||
func startWebRTCSession(room *Room, webRTC *webrtc.WebRTC, playerIndex int) {
|
||||
inputChannel := room.inputChannel
|
||||
log.Println("room, inputChannel", room, inputChannel)
|
||||
for {
|
||||
select {
|
||||
case <-webRTC.Done:
|
||||
removeSession(webRTC, room)
|
||||
default:
|
||||
}
|
||||
// Client stopped
|
||||
if webRTC.IsClosed() {
|
||||
return
|
||||
|
|
@ -397,3 +388,157 @@ func faninInput(inputChannel chan int, webRTC *webrtc.WebRTC, playerIndex int) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func cleanSession(w *webrtc.WebRTC) {
|
||||
room, ok := rooms[w.RoomID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
removeSession(w, room)
|
||||
}
|
||||
|
||||
func removeSession(w *webrtc.WebRTC, room *Room) {
|
||||
room.sessionsLock.Lock()
|
||||
defer room.sessionsLock.Unlock()
|
||||
for i, s := range room.rtcSessions {
|
||||
if s == w {
|
||||
room.rtcSessions = append(room.rtcSessions[:i], room.rtcSessions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
// If room has no sessions, close room
|
||||
if len(room.rtcSessions) == 0 {
|
||||
room.Done <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func getServerIDOfRoom(oc *Client, roomID string) string {
|
||||
log.Println("Request overlord roomID")
|
||||
packet := oc.syncSend(
|
||||
WSPacket{
|
||||
ID: "getRoom",
|
||||
Data: roomID,
|
||||
},
|
||||
)
|
||||
log.Println("Received roomID from overlord")
|
||||
|
||||
return packet.Data
|
||||
}
|
||||
|
||||
func bridgeConnection(session *Session, serverID string, gameName string, roomID string, playerIndex int) {
|
||||
log.Println("Bridging connection to other Host ", serverID)
|
||||
client := session.client
|
||||
// Ask client to init
|
||||
|
||||
log.Println("Requesting offer to browser", serverID)
|
||||
resp := client.syncSend(WSPacket{
|
||||
ID: "requestOffer",
|
||||
Data: "",
|
||||
})
|
||||
|
||||
log.Println("Sending offer to overlord to relay message to target host", resp.TargetHostID)
|
||||
// Ask overlord to relay SDP packet to serverID
|
||||
resp.TargetHostID = serverID
|
||||
remoteTargetSDP := oclient.syncSend(resp)
|
||||
log.Println("Got back remote host SDP, sending to browser")
|
||||
// Send back remote SDP of remote server to browser
|
||||
//client.syncSend(WSPacket{
|
||||
//ID: "sdp",
|
||||
//Data: remoteTargetSDP.Data,
|
||||
//})
|
||||
client.send(WSPacket{
|
||||
ID: "sdp",
|
||||
Data: remoteTargetSDP.Data,
|
||||
}, nil)
|
||||
log.Println("Init session done, start game on target host")
|
||||
|
||||
oclient.syncSend(WSPacket{
|
||||
ID: "start",
|
||||
Data: gameName,
|
||||
TargetHostID: serverID,
|
||||
RoomID: roomID,
|
||||
PlayerIndex: playerIndex,
|
||||
})
|
||||
log.Println("Game is started on remote host")
|
||||
}
|
||||
|
||||
func createOverlordConnection() (*websocket.Conn, error) {
|
||||
c, _, err := websocket.DefaultDialer.Dial(*config.OverlordHost, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func NewOverlordClient() *Client {
|
||||
oc, err := createOverlordConnection()
|
||||
if err != nil {
|
||||
log.Println("Cannot connect to overlord")
|
||||
log.Println("Run as a single server")
|
||||
return nil
|
||||
}
|
||||
oclient := NewClient(oc)
|
||||
|
||||
// Received from overlord the serverID
|
||||
oclient.receive(
|
||||
"serverID",
|
||||
func(response WSPacket) (request WSPacket) {
|
||||
// Stick session with serverID got from overlord
|
||||
log.Println("Received serverID ", response.Data)
|
||||
serverID = response.Data
|
||||
|
||||
return EmptyPacket
|
||||
},
|
||||
)
|
||||
|
||||
// Received from overlord the sdp. This is happens when bridging
|
||||
// TODO: refactor
|
||||
oclient.receive(
|
||||
"initwebrtc",
|
||||
func(resp WSPacket) (req WSPacket) {
|
||||
log.Println("Received a sdp request from overlord")
|
||||
log.Println("Start peerconnection from the sdp")
|
||||
peerconnection := webrtc.NewWebRTC()
|
||||
// init new peerconnection from sessionID
|
||||
localSession, err := peerconnection.StartClient(resp.Data, width, height)
|
||||
peerconnections[resp.SessionID] = peerconnection
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
return WSPacket{
|
||||
ID: "sdp",
|
||||
Data: localSession,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
// Received start from overlord. This is happens when bridging
|
||||
// TODO: refactor
|
||||
oclient.receive(
|
||||
"start",
|
||||
func(resp WSPacket) (req WSPacket) {
|
||||
log.Println("Received a start request from overlord")
|
||||
log.Println("Add the connection to current room on the host")
|
||||
|
||||
peerconnection := peerconnections[resp.SessionID]
|
||||
roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex)
|
||||
// Bridge always access to old room
|
||||
// TODO: log warn
|
||||
if isNewRoom == true {
|
||||
log.Fatal("Bridge should not spawn new room")
|
||||
}
|
||||
|
||||
req.ID = "start"
|
||||
req.RoomID = roomID
|
||||
return req
|
||||
},
|
||||
)
|
||||
// heartbeat to keep pinging overlord. We not ping from server to browser, so we don't call heartbeat in browserClient
|
||||
go oclient.heartbeat()
|
||||
go oclient.listen()
|
||||
|
||||
return oclient
|
||||
}
|
||||
123
overlord.go
Normal file
123
overlord.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/giongto35/cloud-game/webrtc"
|
||||
)
|
||||
|
||||
var roomToServer = map[string]string{}
|
||||
|
||||
// servers are the map serverID to server Client
|
||||
var servers = map[string]*Client{}
|
||||
|
||||
// If it's overlord, handle overlord connection (from host to overlord)
|
||||
func wso(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Println("Connected")
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Print("[!] WS upgrade:", err)
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
// register new server
|
||||
serverID := strconv.Itoa(rand.Int())
|
||||
log.Println("A new server connected ", serverID)
|
||||
|
||||
client := NewClient(c)
|
||||
servers[serverID] = client
|
||||
|
||||
wssession := &Session{
|
||||
client: client,
|
||||
peerconnection: webrtc.NewWebRTC(),
|
||||
// The server session is maintaining
|
||||
}
|
||||
|
||||
client.send(
|
||||
WSPacket{
|
||||
ID: "serverID",
|
||||
Data: serverID,
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
client.receive("registerRoom", func(resp WSPacket) WSPacket {
|
||||
log.Println("Received registerRoom ", resp.Data, serverID)
|
||||
roomToServer[resp.Data] = serverID
|
||||
return WSPacket{
|
||||
ID: "registerRoom",
|
||||
}
|
||||
})
|
||||
|
||||
client.receive("getRoom", func(resp WSPacket) WSPacket {
|
||||
log.Println("Received a getroom request")
|
||||
return WSPacket{
|
||||
ID: "getRoom",
|
||||
Data: roomToServer[resp.Data],
|
||||
}
|
||||
})
|
||||
|
||||
client.receive("initwebrtc", func(resp WSPacket) WSPacket {
|
||||
log.Println("Received a relay sdp request from a host")
|
||||
// TODO: Abstract
|
||||
if resp.TargetHostID != serverID {
|
||||
log.Println("sending relay sdp to target host", resp)
|
||||
// relay SDP to target host and get back sdp
|
||||
// TODO: Async
|
||||
sdp := servers[resp.TargetHostID].syncSend(
|
||||
resp,
|
||||
)
|
||||
|
||||
return sdp
|
||||
}
|
||||
log.Println("Target host is overlord itself: start peerconnection")
|
||||
// If the target is in master
|
||||
// start by its old
|
||||
localSession, err := wssession.peerconnection.StartClient(resp.Data, width, height)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
return WSPacket{
|
||||
ID: "sdp",
|
||||
Data: localSession,
|
||||
}
|
||||
})
|
||||
|
||||
// TODO: use relay ID type
|
||||
// TODO: Merge sdp and start
|
||||
client.receive("start", func(resp WSPacket) WSPacket {
|
||||
log.Println("Received a relay start request from a host")
|
||||
// TODO: Abstract
|
||||
if resp.TargetHostID != serverID {
|
||||
// relay SDP to target host and get back sdp
|
||||
// TODO: Async
|
||||
resp := servers[resp.TargetHostID].syncSend(
|
||||
resp,
|
||||
)
|
||||
|
||||
return resp
|
||||
}
|
||||
log.Println("Target host is overlord itself: start game")
|
||||
// If the target is in master
|
||||
// start by its old
|
||||
roomID, isNewRoom := startSession(wssession.peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex)
|
||||
// Bridge always access to old room
|
||||
// TODO: log warn
|
||||
if isNewRoom == true {
|
||||
log.Fatal("Bridge should not spawn new room")
|
||||
}
|
||||
|
||||
return WSPacket{
|
||||
ID: "start",
|
||||
RoomID: roomID,
|
||||
}
|
||||
})
|
||||
|
||||
client.listen()
|
||||
}
|
||||
1
overlord/overlord.go
Normal file
1
overlord/overlord.go
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
2
run_local.sh
vendored
2
run_local.sh
vendored
|
|
@ -2,4 +2,4 @@
|
|||
docker build . -t cloud-game-local
|
||||
docker stop cloud-game-local
|
||||
docker rm cloud-game-local
|
||||
docker run --privileged -d --name cloud-game-local -p 8000:8000 cloud-game
|
||||
docker run --privileged -d --name cloud-game-local -p 8000:8000 cloud-game-local cloud-game
|
||||
|
|
|
|||
9
static/gameboy.html
vendored
9
static/gameboy.html
vendored
|
|
@ -37,7 +37,8 @@
|
|||
<div id="glass-gameboy-text">GAME BOY</div>
|
||||
<div id="glass-color-text">C</div>
|
||||
<div id="screen">
|
||||
<video id="game-screen" autoplay=true poster="https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif">
|
||||
<!--<video id="game-screen" autoplay=true poster="https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif">-->
|
||||
<video id="game-screen" autoplay=true>
|
||||
</video>
|
||||
|
||||
<div id="menu-screen">
|
||||
|
|
@ -88,9 +89,9 @@
|
|||
<br />
|
||||
Game: <br />
|
||||
Use Up, Down, Left, Right to Move <br />
|
||||
Z to jump (A) <br />
|
||||
X to sprint (B) <br />
|
||||
C is start (in game) <br />
|
||||
Z (A butotn) <br />
|
||||
X (B button) <br />
|
||||
C is start (or pause in some games) <br />
|
||||
V is select game <br />
|
||||
Q is super quit <br />
|
||||
S to save <br />
|
||||
|
|
|
|||
6
static/index_ws.html
vendored
6
static/index_ws.html
vendored
|
|
@ -13,7 +13,7 @@ textarea {
|
|||
<button id="play" onclick="window.startGame()">Play</button>
|
||||
<button id="play" onclick="pc.close()">Stop</button>
|
||||
<button id="play" onclick="window.hihi()">Magic</button><br/><br/>
|
||||
Your current room: <b><label id="currentRoomID" style="color:white"></b> <br />
|
||||
Your current room: <b><label id="currentRoomID" style="color:blue"></b> <br />
|
||||
You can join a remote game by roomID.<br />
|
||||
Room ID: <input type="text" id="roomID">
|
||||
Play as player(1,2): <select id="playerIndex">
|
||||
|
|
@ -35,8 +35,8 @@ Play as player(1,2): <select id="playerIndex">
|
|||
|
||||
Player 1
|
||||
Use Up, Down, Left, Right to Move <br />
|
||||
Z to jump (A) <br />
|
||||
X to sprint (B) <br />
|
||||
Z to (A) <br />
|
||||
X to (B) <br />
|
||||
S to save <br />
|
||||
L to load <br />
|
||||
<!--Fullscreen media for better gaming experience<br /-->
|
||||
|
|
|
|||
1
static/js/const.js
vendored
1
static/js/const.js
vendored
|
|
@ -99,4 +99,5 @@ KEY_BIT = ["a", "b", "select", "start", "up", "down", "left", "right"];
|
|||
|
||||
|
||||
INPUT_FPS = 100;
|
||||
PINGPONGPS = 5;
|
||||
INPUT_STATE_PACKET = 5;
|
||||
|
|
|
|||
32
static/js/gameboy_controller.js
vendored
32
static/js/gameboy_controller.js
vendored
|
|
@ -3,23 +3,17 @@
|
|||
function showMenuScreen() {
|
||||
log("Clean up connection / frame");
|
||||
// clean up before / after menu
|
||||
try {
|
||||
inputChannel.close();
|
||||
} catch (err) {
|
||||
log(`> [Warning] input channel: ${err}`);
|
||||
}
|
||||
//try {
|
||||
//inputChannel.gameboyIndeoclose();
|
||||
//} catch (err) {
|
||||
//log(`> [Warning] peer connection: ${err}`);
|
||||
//}
|
||||
|
||||
try {
|
||||
pc.close();
|
||||
} catch (err) {
|
||||
log(`> [Warning] peer connection: ${err}`);
|
||||
}
|
||||
|
||||
try {
|
||||
conn.close();
|
||||
} catch (err) {
|
||||
log(`> [Warning] Websocket connection: ${err}`);
|
||||
}
|
||||
//try {
|
||||
//conn.close();
|
||||
//} catch (err) {
|
||||
//log(`> [Warning] Websocket connection: ${err}`);
|
||||
//}
|
||||
|
||||
$("#game-screen").hide();
|
||||
if (!DEBUG) {
|
||||
|
|
@ -69,7 +63,6 @@ function chooseGame(idx, force=false) {
|
|||
function setState(e, bo) {
|
||||
if (e.keyCode in KEY_MAP) {
|
||||
keyState[KEY_MAP[e.keyCode]] = bo;
|
||||
stateUnchange = false;
|
||||
unchangePacket = INPUT_STATE_PACKET;
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +83,7 @@ document.body.onkeyup = function (e) {
|
|||
}
|
||||
} else if (screenState === "game") {
|
||||
setState(e, false);
|
||||
|
||||
|
||||
switch (KEY_MAP[e.keyCode]) {
|
||||
case "save":
|
||||
conn.send(JSON.stringify({"id": "save", "data": ""}));
|
||||
|
|
@ -155,7 +148,7 @@ document.body.onkeydown = function (e) {
|
|||
|
||||
function sendInput() {
|
||||
// prepare key
|
||||
if (stateUnchange || unchangePacket > 0) {
|
||||
if (unchangePacket > 0) {
|
||||
st = "";
|
||||
KEY_BIT.slice().reverse().forEach(elem => {
|
||||
st += keyState[elem] ? 1 : 0;
|
||||
|
|
@ -168,7 +161,6 @@ function sendInput() {
|
|||
a[0] = ss;
|
||||
inputChannel.send(a);
|
||||
|
||||
stateUnchange = false;
|
||||
unchangePacket--;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
static/js/global.js
vendored
3
static/js/global.js
vendored
|
|
@ -32,7 +32,6 @@ keyState = {
|
|||
quit: false
|
||||
}
|
||||
|
||||
stateUnchange = true;
|
||||
unchangePacket = INPUT_STATE_PACKET;
|
||||
inputTimer = null;
|
||||
|
||||
|
|
@ -53,4 +52,4 @@ function log(msg) {
|
|||
document.getElementById('div').innerHTML += msg + '<br>'
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
168
static/js/ws.js
vendored
168
static/js/ws.js
vendored
|
|
@ -1,69 +1,80 @@
|
|||
var pc;
|
||||
var curPacketID = "";
|
||||
var curSessionID = "";
|
||||
// web socket
|
||||
|
||||
function startGame() {
|
||||
log("Starting game screen");
|
||||
conn = new WebSocket(`ws://${location.host}/ws`);
|
||||
|
||||
// clear
|
||||
endInput();
|
||||
document.getElementById('div').innerHTML = "";
|
||||
if (!DEBUG) {
|
||||
$("#menu-screen").fadeOut(400, function() {
|
||||
$("#game-screen").show();
|
||||
});
|
||||
// Clear old roomID
|
||||
conn.onopen = () => {
|
||||
log("WebSocket is opened. Send ping");
|
||||
log("Send ping pong frequently")
|
||||
// pingpongTimer = setInterval(sendPing, 1000 / PINGPONGPS)
|
||||
|
||||
startWebRTC();
|
||||
}
|
||||
|
||||
conn.onerror = error => {
|
||||
log(`Websocket error: ${error}`);
|
||||
}
|
||||
|
||||
conn.onclose = () => {
|
||||
log("Websocket closed");
|
||||
}
|
||||
|
||||
conn.onmessage = e => {
|
||||
d = JSON.parse(e.data);
|
||||
switch (d["id"]) {
|
||||
case "sdp":
|
||||
log("Got remote sdp");
|
||||
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
|
||||
//conn.send(JSON.stringify({"id": "sdpdon", "packet_id": d["packet_id"]}));
|
||||
break;
|
||||
case "requestOffer":
|
||||
curPacketID = d["packet_id"];
|
||||
log("Received request offer ", curPacketID)
|
||||
startWebRTC();
|
||||
//pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: false}).then(d => {
|
||||
//pc.setLocalDescription(d).catch(log);
|
||||
//})
|
||||
|
||||
//case "sdpremote":
|
||||
//log("Got remote sdp");
|
||||
//pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
|
||||
//conn.send(JSON.stringify({"id": "remotestart", "data": GAME_LIST[gameIdx].nes, "room_id": roomID.value, "player_index": parseInt(playerIndex.value, 10)}));inputTimer
|
||||
//break;
|
||||
case "heartbeat":
|
||||
console.log("Ping: ", Date.now() - d["data"])
|
||||
// TODO: Calc time
|
||||
break;
|
||||
case "start":
|
||||
log("Got start");
|
||||
roomID.value = ""
|
||||
currentRoomID.innerText = d["room_id"]
|
||||
break;
|
||||
case "save":
|
||||
log(`Got save response: ${d["data"]}`);
|
||||
break;
|
||||
case "load":
|
||||
log(`Got load response: ${d["data"]}`);
|
||||
break;
|
||||
}
|
||||
// end clear
|
||||
}
|
||||
|
||||
conn = new WebSocket(`ws://${location.host}/ws`);
|
||||
|
||||
// Clear old roomID
|
||||
conn.onopen = () => {
|
||||
log("WebSocket is opened. Send ping");
|
||||
conn.send(JSON.stringify({"id": "ping", "data": GAME_LIST[gameIdx].nes, "room_id": roomID.value, "player_index": parseInt(playerIndex.value, 10)}));
|
||||
}
|
||||
|
||||
conn.onerror = error => {
|
||||
log(`Websocket error: ${error}`);
|
||||
}
|
||||
|
||||
conn.onclose = () => {
|
||||
log("Websocket closed");
|
||||
}
|
||||
|
||||
conn.onmessage = e => {
|
||||
d = JSON.parse(e.data);
|
||||
switch (d["id"]) {
|
||||
case "pong":
|
||||
log("Recv pong. Start webrtc");
|
||||
startWebRTC();
|
||||
break;
|
||||
case "sdp":
|
||||
log("Got remote sdp");
|
||||
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
|
||||
break;
|
||||
case "start":
|
||||
log("Got start");
|
||||
roomID.value = ""
|
||||
currentRoomID.innerText = d["room_id"]
|
||||
break;
|
||||
case "save":
|
||||
log(`Got save response: ${d["data"]}`);
|
||||
break;
|
||||
case "load":
|
||||
log(`Got load response: ${d["data"]}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function sendPing() {
|
||||
// TODO: format the package with time
|
||||
conn.send(JSON.stringify({"id": "heartbeat", "data": Date.now().toString()}));
|
||||
}
|
||||
|
||||
function startWebRTC() {
|
||||
// webrtc
|
||||
pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
|
||||
// input channel
|
||||
inputChannel = pc.createDataChannel('foo')
|
||||
|
||||
// input channel, ordered + reliable
|
||||
inputChannel = pc.createDataChannel('a', {
|
||||
ordered: true,
|
||||
});
|
||||
inputChannel.onclose = () => log('inputChannel has closed');
|
||||
inputChannel.onopen = () => log('inputChannel has opened');
|
||||
inputChannel.onmessage = e => {
|
||||
console.log(e);
|
||||
log(`Message '${e.data}'`);
|
||||
}
|
||||
|
||||
window.AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
var context = new AudioContext();
|
||||
|
|
@ -123,14 +134,11 @@ function startGame() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
log(`iceConnectionState: ${pc.iceConnectionState}`);
|
||||
|
||||
if (pc.iceConnectionState === "connected") {
|
||||
conn.send(JSON.stringify({"id": "start", "data": ""}));
|
||||
startInput();
|
||||
screenState = "game";
|
||||
//conn.send(JSON.stringify({"id": "start", "data": ""}));
|
||||
}
|
||||
else if (pc.iceConnectionState === "disconnected") {
|
||||
endInput();
|
||||
|
|
@ -165,20 +173,38 @@ function startGame() {
|
|||
session = btoa(JSON.stringify(pc.localDescription));
|
||||
localSessionDescription = session;
|
||||
log("Send SDP to remote peer");
|
||||
conn.send(JSON.stringify({"id": "sdp", "data": session}));
|
||||
// TODO: Fix curPacketID
|
||||
conn.send(JSON.stringify({"id": "initwebrtc", "data": session, "packet_id": curPacketID}));
|
||||
} else {
|
||||
console.log(JSON.stringify(event.candidate));
|
||||
}
|
||||
}
|
||||
|
||||
function startWebRTC() {
|
||||
// receiver only tracks
|
||||
pc.addTransceiver('video', {'direction': 'recvonly'});
|
||||
pc.addTransceiver('audio', {'direction': 'recvonly'});
|
||||
// receiver only tracks
|
||||
pc.addTransceiver('video', {'direction': 'recvonly'});
|
||||
|
||||
// create SDP
|
||||
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: false}).then(d => {
|
||||
pc.setLocalDescription(d).catch(log);
|
||||
})
|
||||
|
||||
// create SDP
|
||||
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).then(d => {
|
||||
pc.setLocalDescription(d).catch(log);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
log("Starting game screen");
|
||||
screenState = "game";
|
||||
|
||||
conn.send(JSON.stringify({"id": "start", "data": GAME_LIST[gameIdx].nes, "room_id": roomID.value, "player_index": parseInt(playerIndex.value, 10)}));
|
||||
|
||||
// clear menu screen
|
||||
endInput();
|
||||
document.getElementById('div').innerHTML = "";
|
||||
if (!DEBUG) {
|
||||
$("#menu-screen").fadeOut(400, function() {
|
||||
$("#game-screen").show();
|
||||
});
|
||||
}
|
||||
// end clear
|
||||
|
||||
// startInput();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,24 +10,26 @@ import (
|
|||
)
|
||||
|
||||
type Director struct {
|
||||
view *GameView
|
||||
timestamp float64
|
||||
imageChannel chan *image.RGBA
|
||||
audioChanel chan float32
|
||||
inputChannel chan int
|
||||
closedChannel chan bool
|
||||
roomID string
|
||||
hash string
|
||||
// audio *Audio
|
||||
view *GameView
|
||||
timestamp float64
|
||||
imageChannel chan *image.RGBA
|
||||
audioChannel chan float32
|
||||
inputChannel chan int
|
||||
Done chan struct{}
|
||||
|
||||
roomID string
|
||||
hash string
|
||||
}
|
||||
|
||||
const FPS = 60
|
||||
|
||||
func NewDirector(roomID string, imageChannel chan *image.RGBA, audioChanel chan float32, inputChannel chan int, closedChannel chan bool) *Director {
|
||||
func NewDirector(roomID string, imageChannel chan *image.RGBA, audioChannel chan float32, inputChannel chan int) *Director {
|
||||
director := Director{}
|
||||
director.Done = make(chan struct{})
|
||||
director.audioChannel = audioChannel
|
||||
director.imageChannel = imageChannel
|
||||
director.audioChanel = audioChanel
|
||||
director.inputChannel = inputChannel
|
||||
director.closedChannel = closedChannel
|
||||
director.roomID = roomID
|
||||
director.hash = ""
|
||||
return &director
|
||||
|
|
@ -44,6 +46,10 @@ func (d *Director) SetView(view *GameView) {
|
|||
d.timestamp = float64(time.Now().Nanosecond()) / float64(time.Second)
|
||||
}
|
||||
|
||||
func (d *Director) UpdateInput(input int) {
|
||||
d.view.UpdateInput(input)
|
||||
}
|
||||
|
||||
func (d *Director) Step() {
|
||||
timestamp := float64(time.Now().Nanosecond()) / float64(time.Second)
|
||||
dt := timestamp - d.timestamp
|
||||
|
|
@ -76,7 +82,9 @@ L:
|
|||
|
||||
select {
|
||||
// if there is event from close channel => the game is ended
|
||||
case <-d.closedChannel:
|
||||
case input := <-d.inputChannel:
|
||||
d.UpdateInput(input)
|
||||
case <-d.Done:
|
||||
break L
|
||||
default:
|
||||
}
|
||||
|
|
@ -98,12 +106,13 @@ func (d *Director) PlayGame(path string) {
|
|||
log.Fatalln(err)
|
||||
}
|
||||
// Set GameView as current view
|
||||
d.SetView(NewGameView(console, path, hash, d.imageChannel, d.audioChanel, d.inputChannel))
|
||||
d.SetView(NewGameView(console, path, hash, d.imageChannel, d.audioChannel, d.inputChannel))
|
||||
}
|
||||
|
||||
func (d *Director) SaveGame() error {
|
||||
if d.hash != "" {
|
||||
return d.view.console.SaveState(savePath(d.hash))
|
||||
d.view.Save(d.hash)
|
||||
return nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -111,8 +120,9 @@ func (d *Director) SaveGame() error {
|
|||
|
||||
func (d *Director) LoadGame() error {
|
||||
if d.hash != "" {
|
||||
return d.view.console.LoadState(savePath(d.hash))
|
||||
d.view.Load(d.hash)
|
||||
return nil
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ui
|
|||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"github.com/giongto35/cloud-game/nes"
|
||||
)
|
||||
|
||||
|
|
@ -27,55 +28,60 @@ const (
|
|||
right2
|
||||
)
|
||||
const NumKeys = 8
|
||||
const SampleRate = 16000
|
||||
const Channels = 1
|
||||
const TimeFrame = 60
|
||||
|
||||
// Audio consts
|
||||
const (
|
||||
SampleRate = 16000
|
||||
Channels = 1
|
||||
TimeFrame = 60
|
||||
)
|
||||
|
||||
|
||||
type GameView struct {
|
||||
console *nes.Console
|
||||
title string
|
||||
hash string
|
||||
console *nes.Console
|
||||
title string
|
||||
hash string
|
||||
|
||||
// equivalent to the list key pressed const above
|
||||
keyPressed [NumKeys * 2]bool
|
||||
|
||||
savingPath string
|
||||
loadingPath string
|
||||
|
||||
imageChannel chan *image.RGBA
|
||||
audioChanel chan float32
|
||||
audioChannel chan float32
|
||||
inputChannel chan int
|
||||
}
|
||||
|
||||
|
||||
func NewGameView(console *nes.Console, title, hash string, imageChannel chan *image.RGBA, audioChanel chan float32, inputChannel chan int) *GameView {
|
||||
func NewGameView(console *nes.Console, title, hash string, imageChannel chan *image.RGBA, audioChannel chan float32, inputChannel chan int) *GameView {
|
||||
gameview := &GameView{
|
||||
console: console,
|
||||
title: title,
|
||||
hash: hash,
|
||||
keyPressed: [NumKeys * 2]bool{false},
|
||||
imageChannel: imageChannel,
|
||||
audioChanel: audioChanel,
|
||||
audioChannel: audioChannel,
|
||||
inputChannel: inputChannel,
|
||||
}
|
||||
|
||||
go gameview.ListenToInputChannel()
|
||||
return gameview
|
||||
}
|
||||
|
||||
// ListenToInputChannel listen from input channel streamm, which is exposed to WebRTC session
|
||||
func (view *GameView) ListenToInputChannel() {
|
||||
for {
|
||||
keysInBinary := <-view.inputChannel
|
||||
for i := 0; i < NumKeys*2; i++ {
|
||||
b := ((keysInBinary & 1) == 1)
|
||||
view.keyPressed[i] = (view.keyPressed[i] && b) || b
|
||||
keysInBinary = keysInBinary >> 1
|
||||
}
|
||||
func (view *GameView) UpdateInput(keysInBinary int) {
|
||||
for i := 0; i < NumKeys*2; i++ {
|
||||
b := ((keysInBinary & 1) == 1)
|
||||
view.keyPressed[i] = (view.keyPressed[i] && b) || b
|
||||
keysInBinary = keysInBinary >> 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Enter enter the game view.
|
||||
func (view *GameView) Enter() {
|
||||
view.console.SetAudioSampleRate(SampleRate)
|
||||
view.console.SetAudioChannel(view.audioChanel)
|
||||
view.console.SetAudioChannel(view.audioChannel)
|
||||
|
||||
// load state
|
||||
if err := view.console.LoadState(savePath(view.hash)); err == nil {
|
||||
|
|
@ -112,12 +118,36 @@ func (view *GameView) Update(t, dt float64) {
|
|||
}
|
||||
console := view.console
|
||||
view.updateControllers()
|
||||
view.UpdateEvents()
|
||||
console.StepSeconds(dt)
|
||||
|
||||
// fps to set frame
|
||||
view.imageChannel <- console.Buffer()
|
||||
}
|
||||
|
||||
func (view *GameView) Save(hash string) {
|
||||
// put saving event to queue, process in updateEvent
|
||||
view.savingPath = savePath(view.hash)
|
||||
}
|
||||
|
||||
func (view *GameView) Load(path string) {
|
||||
// put saving event to queue, process in updateEvent
|
||||
view.loadingPath = savePath(view.hash)
|
||||
}
|
||||
|
||||
func (view *GameView) UpdateEvents() {
|
||||
// If there is saving event, save and discard the save event
|
||||
if view.savingPath != "" {
|
||||
view.console.SaveState(view.savingPath)
|
||||
view.savingPath = ""
|
||||
}
|
||||
// If there is loading event, save and discard the load event
|
||||
if view.loadingPath != "" {
|
||||
view.console.LoadState(view.loadingPath)
|
||||
view.loadingPath = ""
|
||||
}
|
||||
}
|
||||
|
||||
func (view *GameView) updateControllers() {
|
||||
// Divide keyPressed to player 1 and player 2
|
||||
// First 8 keys are player 1
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ package vpxencoder
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/giongto35/cloud-game/config"
|
||||
)
|
||||
|
||||
// https://chromium.googlesource.com/webm/libvpx/+/master/examples/simple_encoder.c
|
||||
|
|
@ -113,6 +117,8 @@ func (v *VpxEncoder) init() error {
|
|||
func (v *VpxEncoder) startLooping() {
|
||||
go func() {
|
||||
for {
|
||||
beginEncoding := time.Now()
|
||||
|
||||
yuv := <-v.Input
|
||||
// Add Image
|
||||
v.vpxCodexIter = nil
|
||||
|
|
@ -140,6 +146,10 @@ func (v *VpxEncoder) startLooping() {
|
|||
}
|
||||
v.Output <- bs
|
||||
}
|
||||
|
||||
if *config.IsMonitor {
|
||||
log.Println("Encoding time: ", time.Now().Sub(beginEncoding))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,19 @@ import (
|
|||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/config"
|
||||
vpxEncoder "github.com/giongto35/cloud-game/vpx-encoder"
|
||||
"github.com/pion/webrtc"
|
||||
"github.com/pion/webrtc/pkg/media"
|
||||
)
|
||||
|
||||
var config = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}}
|
||||
var webrtcconfig = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}}
|
||||
|
||||
// Allows compressing offer/answer to bypass terminal input limits.
|
||||
const compress = false
|
||||
|
|
@ -99,6 +101,11 @@ func NewWebRTC() *WebRTC {
|
|||
return w
|
||||
}
|
||||
|
||||
type InputDataPair struct {
|
||||
data int
|
||||
time time.Time
|
||||
}
|
||||
|
||||
// WebRTC connection
|
||||
type WebRTC struct {
|
||||
connection *webrtc.PeerConnection
|
||||
|
|
@ -109,6 +116,12 @@ type WebRTC struct {
|
|||
ImageChannel chan []byte
|
||||
AudioChannel chan []byte
|
||||
InputChannel chan int
|
||||
|
||||
Done chan struct{}
|
||||
lastTime time.Time
|
||||
curFPS int
|
||||
|
||||
RoomID string
|
||||
}
|
||||
|
||||
// StartClient start webrtc
|
||||
|
|
@ -134,11 +147,7 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
|
|||
|
||||
log.Println("=== StartClient ===")
|
||||
|
||||
webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000)
|
||||
webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000)
|
||||
|
||||
w.connection, err = webrtc.NewPeerConnection(config)
|
||||
|
||||
w.connection, err = webrtc.NewPeerConnection(webrtcconfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
@ -152,10 +161,13 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
|
|||
return "", err
|
||||
}
|
||||
|
||||
|
||||
// dd := false
|
||||
// audioTrack, err := w.connection.CreateDataChannel("foo2", &webrtc.DataChannelInit{Ordered: &dd})
|
||||
audioTrack, err := w.connection.CreateDataChannel("foo2", nil)
|
||||
// audio track
|
||||
dfalse := false
|
||||
var d0 uint16 = 0
|
||||
audioTrack, err := w.connection.CreateDataChannel("b", &webrtc.DataChannelInit{
|
||||
Ordered: &dfalse,
|
||||
MaxRetransmits: &d0,
|
||||
})
|
||||
|
||||
// WebRTC state callback
|
||||
w.connection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
|
||||
|
|
@ -190,11 +202,18 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
|
|||
|
||||
// Register text message handling
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
//layout .:= "2006-01-02T15:04:05.000Z"
|
||||
//if t, err := time.Parse(layout, string(msg.Data[1])); err == nil {
|
||||
//fmt.Println("Delay ", time.Now().Sub(t))
|
||||
//} else {
|
||||
w.InputChannel <- int(msg.Data[0])
|
||||
//}
|
||||
})
|
||||
|
||||
d.OnClose(func() {
|
||||
w.isClosed = true
|
||||
fmt.Println("closed webrtc")
|
||||
w.Done <- struct{}{}
|
||||
close(w.Done)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -216,6 +235,10 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
|
|||
return localSession, nil
|
||||
}
|
||||
|
||||
func (w *WebRTC) AttachRoomID(roomID string) {
|
||||
w.RoomID = roomID
|
||||
}
|
||||
|
||||
// TODO: Take a look at this
|
||||
func (w *WebRTC) AddCandidate(candidate webrtc.ICECandidateInit) {
|
||||
err := w.connection.AddICECandidate(candidate)
|
||||
|
|
@ -264,6 +287,9 @@ func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, audioTrack *webrtc.DataC
|
|||
go func() {
|
||||
for w.isConnected {
|
||||
bs := <-w.encoder.Output
|
||||
if *config.IsMonitor {
|
||||
log.Println("FPS : ", w.calculateFPS())
|
||||
}
|
||||
vp8Track.WriteSample(media.Sample{Data: bs, Samples: 1})
|
||||
}
|
||||
}()
|
||||
|
|
@ -279,3 +305,11 @@ func (w *WebRTC) startStreaming(vp8Track *webrtc.Track, audioTrack *webrtc.DataC
|
|||
}()
|
||||
|
||||
}
|
||||
|
||||
func (w *WebRTC) calculateFPS() int {
|
||||
elapsedTime := time.Now().Sub(w.lastTime)
|
||||
w.lastTime = time.Now()
|
||||
curFPS := time.Second / elapsedTime
|
||||
w.curFPS = int(float32(w.curFPS)*0.9 + float32(curFPS)*0.1)
|
||||
return w.curFPS
|
||||
}
|
||||
|
|
|
|||
150
ws.go
Normal file
150
ws.go
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
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)
|
||||
}
|
||||
|
||||
type WSPacket struct {
|
||||
ID string `json:"id"`
|
||||
Data string `json:"data"`
|
||||
|
||||
RoomID string `json:"room_id"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
|
||||
TargetHostID string `json:"target_id"`
|
||||
PacketID string `json:"packet_id"`
|
||||
// Globally ID of a session
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
var EmptyPacket = WSPacket{}
|
||||
|
||||
func NewClient(conn *websocket.Conn) *Client {
|
||||
sendCallback := map[string]func(WSPacket){}
|
||||
recvCallback := map[string]func(WSPacket){}
|
||||
return &Client{
|
||||
conn: conn,
|
||||
|
||||
sendCallback: sendCallback,
|
||||
recvCallback: recvCallback,
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
c.sendLock.Lock()
|
||||
c.conn.WriteMessage(websocket.TextMessage, data)
|
||||
c.sendLock.Unlock()
|
||||
wrapperCallback := func(resp WSPacket) {
|
||||
resp.PacketID = request.PacketID
|
||||
resp.SessionID = request.SessionID
|
||||
callback(resp)
|
||||
}
|
||||
if callback == nil {
|
||||
return
|
||||
}
|
||||
c.sendCallbackLock.Lock()
|
||||
c.sendCallback[request.PacketID] = wrapperCallback
|
||||
c.sendCallbackLock.Unlock()
|
||||
}
|
||||
|
||||
// receive receive and response back
|
||||
func (c *Client) receive(id string, f func(response WSPacket) (request WSPacket)) {
|
||||
c.recvCallback[id] = func(response WSPacket) {
|
||||
req := f(response)
|
||||
// Add Meta data
|
||||
req.PacketID = response.PacketID
|
||||
req.SessionID = response.SessionID
|
||||
|
||||
// Skip rqeuest if it is EmptyPacket
|
||||
if req == EmptyPacket {
|
||||
return
|
||||
}
|
||||
resp, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
log.Println("[!] json marshal error:", err)
|
||||
}
|
||||
//c.conn.SetWriteDeadline(time.Now().Add(writeWait))
|
||||
c.sendLock.Lock()
|
||||
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
|
||||
}
|
||||
|
||||
// heartbeat maintains connection to server
|
||||
func (c *Client) heartbeat() {
|
||||
// send heartbeat every 1s
|
||||
timer := time.Tick(time.Second)
|
||||
|
||||
for range timer {
|
||||
c.send(WSPacket{ID: "heartbeat"}, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) listen() {
|
||||
for {
|
||||
// log.Println("Waiting for message ...")
|
||||
_, rawMsg, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Println("[!] read:", err)
|
||||
break
|
||||
}
|
||||
wspacket := WSPacket{}
|
||||
err = json.Unmarshal(rawMsg, &wspacket)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue