From 80f63f54690cc04cba5fe3fbe4c915a4681f8bf4 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Thanh Date: Sat, 27 Apr 2019 21:42:00 +0700 Subject: [PATCH 01/23] restructure file --- main.go => cmd/main.go | 0 main_test.go => cmd/main_test.go | 0 overlord.go | 123 ------------------------------- overlord/overlord.go | 122 ++++++++++++++++++++++++++++++ {nes => ui/nes}/apu.go | 0 {nes => ui/nes}/cartridge.go | 0 {nes => ui/nes}/console.go | 0 {nes => ui/nes}/controller.go | 0 {nes => ui/nes}/cpu.go | 0 {nes => ui/nes}/filter.go | 0 {nes => ui/nes}/ines.go | 0 {nes => ui/nes}/mapper.go | 0 {nes => ui/nes}/mapper1.go | 0 {nes => ui/nes}/mapper2.go | 0 {nes => ui/nes}/mapper225.go | 0 {nes => ui/nes}/mapper3.go | 0 {nes => ui/nes}/mapper4.go | 0 {nes => ui/nes}/mapper7.go | 0 {nes => ui/nes}/memory.go | 0 {nes => ui/nes}/palette.go | 0 {nes => ui/nes}/ppu.go | 0 ws.go => ws/ws.go | 0 22 files changed, 122 insertions(+), 123 deletions(-) rename main.go => cmd/main.go (100%) rename main_test.go => cmd/main_test.go (100%) delete mode 100644 overlord.go rename {nes => ui/nes}/apu.go (100%) rename {nes => ui/nes}/cartridge.go (100%) rename {nes => ui/nes}/console.go (100%) rename {nes => ui/nes}/controller.go (100%) rename {nes => ui/nes}/cpu.go (100%) rename {nes => ui/nes}/filter.go (100%) rename {nes => ui/nes}/ines.go (100%) rename {nes => ui/nes}/mapper.go (100%) rename {nes => ui/nes}/mapper1.go (100%) rename {nes => ui/nes}/mapper2.go (100%) rename {nes => ui/nes}/mapper225.go (100%) rename {nes => ui/nes}/mapper3.go (100%) rename {nes => ui/nes}/mapper4.go (100%) rename {nes => ui/nes}/mapper7.go (100%) rename {nes => ui/nes}/memory.go (100%) rename {nes => ui/nes}/palette.go (100%) rename {nes => ui/nes}/ppu.go (100%) rename ws.go => ws/ws.go (100%) diff --git a/main.go b/cmd/main.go similarity index 100% rename from main.go rename to cmd/main.go diff --git a/main_test.go b/cmd/main_test.go similarity index 100% rename from main_test.go rename to cmd/main_test.go diff --git a/overlord.go b/overlord.go deleted file mode 100644 index 4a9795f8..00000000 --- a/overlord.go +++ /dev/null @@ -1,123 +0,0 @@ -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() -} diff --git a/overlord/overlord.go b/overlord/overlord.go index 8b137891..4a9795f8 100644 --- a/overlord/overlord.go +++ b/overlord/overlord.go @@ -1 +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() +} diff --git a/nes/apu.go b/ui/nes/apu.go similarity index 100% rename from nes/apu.go rename to ui/nes/apu.go diff --git a/nes/cartridge.go b/ui/nes/cartridge.go similarity index 100% rename from nes/cartridge.go rename to ui/nes/cartridge.go diff --git a/nes/console.go b/ui/nes/console.go similarity index 100% rename from nes/console.go rename to ui/nes/console.go diff --git a/nes/controller.go b/ui/nes/controller.go similarity index 100% rename from nes/controller.go rename to ui/nes/controller.go diff --git a/nes/cpu.go b/ui/nes/cpu.go similarity index 100% rename from nes/cpu.go rename to ui/nes/cpu.go diff --git a/nes/filter.go b/ui/nes/filter.go similarity index 100% rename from nes/filter.go rename to ui/nes/filter.go diff --git a/nes/ines.go b/ui/nes/ines.go similarity index 100% rename from nes/ines.go rename to ui/nes/ines.go diff --git a/nes/mapper.go b/ui/nes/mapper.go similarity index 100% rename from nes/mapper.go rename to ui/nes/mapper.go diff --git a/nes/mapper1.go b/ui/nes/mapper1.go similarity index 100% rename from nes/mapper1.go rename to ui/nes/mapper1.go diff --git a/nes/mapper2.go b/ui/nes/mapper2.go similarity index 100% rename from nes/mapper2.go rename to ui/nes/mapper2.go diff --git a/nes/mapper225.go b/ui/nes/mapper225.go similarity index 100% rename from nes/mapper225.go rename to ui/nes/mapper225.go diff --git a/nes/mapper3.go b/ui/nes/mapper3.go similarity index 100% rename from nes/mapper3.go rename to ui/nes/mapper3.go diff --git a/nes/mapper4.go b/ui/nes/mapper4.go similarity index 100% rename from nes/mapper4.go rename to ui/nes/mapper4.go diff --git a/nes/mapper7.go b/ui/nes/mapper7.go similarity index 100% rename from nes/mapper7.go rename to ui/nes/mapper7.go diff --git a/nes/memory.go b/ui/nes/memory.go similarity index 100% rename from nes/memory.go rename to ui/nes/memory.go diff --git a/nes/palette.go b/ui/nes/palette.go similarity index 100% rename from nes/palette.go rename to ui/nes/palette.go diff --git a/nes/ppu.go b/ui/nes/ppu.go similarity index 100% rename from nes/ppu.go rename to ui/nes/ppu.go diff --git a/ws.go b/ws/ws.go similarity index 100% rename from ws.go rename to ws/ws.go From de8871385566167e10d92a8e9012007daf5251d6 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Thanh Date: Sat, 27 Apr 2019 23:21:14 +0700 Subject: [PATCH 02/23] Add handlers --- games.go => handlers/games.go | 0 handlers/handlers.go | 531 ++++++++++++++++++++++++++++++++++ handlers/overlord.go | 86 ++++++ handlers/rooms.go | 76 +++++ 4 files changed, 693 insertions(+) rename games.go => handlers/games.go (100%) create mode 100644 handlers/handlers.go create mode 100644 handlers/overlord.go create mode 100644 handlers/rooms.go diff --git a/games.go b/handlers/games.go similarity index 100% rename from games.go rename to handlers/games.go diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 00000000..90af04db --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,531 @@ +package handler + +import ( + "encoding/json" + "image" + "io/ioutil" + "log" + "math/rand" + "net/http" + "strconv" + "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" + uuid "github.com/satori/go.uuid" + "gopkg.in/hraban/opus.v2" +) + +// 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{} + +// ID to peerconnection +var peerconnections = map[string]*webrtc.WebRTC{} +var serverID = "" +var oclient *Client + +func getWeb(w http.ResponseWriter, r *http.Request) { + bs, err := ioutil.ReadFile(indexFN) + if err != nil { + log.Fatal(err) + } + w.Write(bs) +} + +// init initilizes a room returns roomID +func initRoom(roomID, gameName string) string { + // if no roomID is given, generate it + if roomID == "" { + roomID = generateRoomID() + } + log.Println("Init new room", roomID, gameName) + imageChannel := make(chan *image.RGBA, 100) + audioChannel := make(chan float32, ui.SampleRate) + inputChannel := make(chan int, 100) + + // create director + director := ui.NewDirector(roomID, imageChannel, audioChannel, inputChannel) + + 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.startVideo() + go room.startAudio() + go director.Start([]string{"games/" + gameName}) + + return roomID +} + +// isRoomRunning check if there is any running sessions. +// TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. +func isRoomRunning(roomID string) bool { + // If no roomID is registered + if _, ok := rooms[roomID]; !ok { + return false + } + + // If there is running session + for _, s := range rooms[roomID].rtcSessions { + if !s.IsClosed() { + return true + } + } + return false +} + +// startSession handles one session call +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) + room := rooms[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 { + log.Print("[!] WS upgrade:", err) + return + } + defer c.Close() + var gameName string + var roomID string + var playerIndex int + + // 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.send(WSPacket{ + ID: "gamelist", + Data: getEncodedGameList(), + }, nil) + + 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 +} + +func (r *Room) startVideo() { + // fanout Screen + for { + select { + case <-r.Done: + r.remove() + return + case image := <-r.imageChannel: + //isRoomRunning := false + + 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.ImageChannel <- yuv + } + //isRoomRunning = true + } + r.sessionsLock.Unlock() + } + } +} + +func (r *Room) startAudio() { + 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 + + // fanout Audio + for { + select { + case <-r.Done: + r.remove() + return + case sample := <-r.audioChannel: + pcm[idx] = sample + idx++ + 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) + + r.sessionsLock.Lock() + for _, webRTC := range r.rtcSessions { + // Client stopped + if webRTC.IsClosed() { + continue + } + + // encode frame + // fanout audioChannel + if webRTC.IsConnected() { + // NOTE: can block here + webRTC.AudioChannel <- data + } + //isRoomRunning = true + } + r.sessionsLock.Unlock() + + idx = 0 + count = (count + 1) & 0xff + + } + } + } +} + +func (r *Room) remove() { + log.Println("Closing room", r) + r.director.Done <- struct{}{} +} + +// 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 + } + + // encode frame + if webRTC.IsConnected() { + input := <-webRTC.InputChannel + // the first 8 bits belong to player 1 + // the next 8 belongs to player 2 ... + // We standardize and put it to inputChannel (16 bits) + input = input << ((uint(playerIndex) - 1) * ui.NumKeys) + inputChannel <- input + } + } +} + +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(oc *websocket.Conn) *Client { + 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] + log.Println("start session") + roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) + log.Println("Done, sending back") + // 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 +} diff --git a/handlers/overlord.go b/handlers/overlord.go new file mode 100644 index 00000000..660d7055 --- /dev/null +++ b/handlers/overlord.go @@ -0,0 +1,86 @@ +package handler + +import ( + "log" + + "github.com/giongto35/cloud-game/config" + "github.com/giongto35/cloud-game/webrtc" + "github.com/gorilla/websocket" +) + +func createOverlordConnection() (*websocket.Conn, error) { + c, _, err := websocket.DefaultDialer.Dial(*config.OverlordHost, nil) + if err != nil { + return nil, err + } + + return c, nil +} + +func NewOverlordClient(oc *websocket.Conn) *Client { + 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] + log.Println("start session") + roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) + log.Println("Done, sending back") + // 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 +} diff --git a/handlers/rooms.go b/handlers/rooms.go new file mode 100644 index 00000000..725cf94f --- /dev/null +++ b/handlers/rooms.go @@ -0,0 +1,76 @@ +package handler + +import ( + "image" + "log" + "sync" + + "github.com/giongto35/cloud-game/ui" + "github.com/giongto35/cloud-game/webrtc" +) + +// 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 + audioChannel chan float32 + inputChannel chan int + // Done channel is to fire exit event when there is no webRTC session running + Done chan struct{} + + rtcSessions []*webrtc.WebRTC + sessionsLock *sync.Mutex + + director *ui.Director +} + +var rooms = map[string]*Room{} + +// init initilizes a room returns roomID +func initRoom(roomID, gameName string) string { + // if no roomID is given, generate it + if roomID == "" { + roomID = generateRoomID() + } + log.Println("Init new room", roomID, gameName) + imageChannel := make(chan *image.RGBA, 100) + audioChannel := make(chan float32, ui.SampleRate) + inputChannel := make(chan int, 100) + + // create director + director := ui.NewDirector(roomID, imageChannel, audioChannel, inputChannel) + + 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.startVideo() + go room.startAudio() + go director.Start([]string{"games/" + gameName}) + + return roomID +} + +// isRoomRunning check if there is any running sessions. +// TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. +func isRoomRunning(roomID string) bool { + // If no roomID is registered + if _, ok := rooms[roomID]; !ok { + return false + } + + // If there is running session + for _, s := range rooms[roomID].rtcSessions { + if !s.IsClosed() { + return true + } + } + return false +} From 1447406b027853f82a1acbc08d9a046463508c98 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Thanh Date: Sun, 28 Apr 2019 21:45:30 +0700 Subject: [PATCH 03/23] WIP --- cmd/main.go | 13 +- config/config.go | 2 + ws/ws.go => cws/cws.go | 16 +- {ui => emulator}/director.go | 6 +- {ui => emulator}/font.go | 2 +- {ui => emulator}/gameview.go | 12 +- {ui => emulator}/nes/apu.go | 0 {ui => emulator}/nes/cartridge.go | 0 {ui => emulator}/nes/console.go | 0 {ui => emulator}/nes/controller.go | 0 {ui => emulator}/nes/cpu.go | 0 {ui => emulator}/nes/filter.go | 0 {ui => emulator}/nes/ines.go | 0 {ui => emulator}/nes/mapper.go | 0 {ui => emulator}/nes/mapper1.go | 0 {ui => emulator}/nes/mapper2.go | 0 {ui => emulator}/nes/mapper225.go | 0 {ui => emulator}/nes/mapper3.go | 0 {ui => emulator}/nes/mapper4.go | 0 {ui => emulator}/nes/mapper7.go | 0 {ui => emulator}/nes/memory.go | 0 {ui => emulator}/nes/palette.go | 0 {ui => emulator}/nes/ppu.go | 0 {ui => emulator}/util.go | 4 +- handlers/client/browser.go | 136 ++++++++ handlers/client/overlord.go | 78 +++++ handlers/client/session.go | 37 ++ handlers/{ => gamelist}/games.go | 9 +- handlers/handlers.go | 543 +++++++---------------------- handlers/media.go | 101 ++++++ handlers/rooms.go | 66 +++- 31 files changed, 573 insertions(+), 452 deletions(-) rename ws/ws.go => cws/cws.go (90%) rename {ui => emulator}/director.go (96%) rename {ui => emulator}/font.go (99%) rename {ui => emulator}/gameview.go (96%) rename {ui => emulator}/nes/apu.go (100%) rename {ui => emulator}/nes/cartridge.go (100%) rename {ui => emulator}/nes/console.go (100%) rename {ui => emulator}/nes/controller.go (100%) rename {ui => emulator}/nes/cpu.go (100%) rename {ui => emulator}/nes/filter.go (100%) rename {ui => emulator}/nes/ines.go (100%) rename {ui => emulator}/nes/mapper.go (100%) rename {ui => emulator}/nes/mapper1.go (100%) rename {ui => emulator}/nes/mapper2.go (100%) rename {ui => emulator}/nes/mapper225.go (100%) rename {ui => emulator}/nes/mapper3.go (100%) rename {ui => emulator}/nes/mapper4.go (100%) rename {ui => emulator}/nes/mapper7.go (100%) rename {ui => emulator}/nes/memory.go (100%) rename {ui => emulator}/nes/palette.go (100%) rename {ui => emulator}/nes/ppu.go (100%) rename {ui => emulator}/util.go (97%) create mode 100644 handlers/client/browser.go create mode 100644 handlers/client/overlord.go create mode 100644 handlers/client/session.go rename handlers/{ => gamelist}/games.go (68%) create mode 100644 handlers/media.go diff --git a/cmd/main.go b/cmd/main.go index d622e4fc..78f8de44 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -15,7 +15,6 @@ import ( "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" @@ -55,7 +54,7 @@ type Room struct { rtcSessions []*webrtc.WebRTC sessionsLock *sync.Mutex - director *ui.Director + director *emulator.Director } var rooms = map[string]*Room{} @@ -134,11 +133,11 @@ func initRoom(roomID, gameName string) string { } log.Println("Init new room", roomID, gameName) imageChannel := make(chan *image.RGBA, 100) - audioChannel := make(chan float32, ui.SampleRate) + audioChannel := make(chan float32, emulator.SampleRate) inputChannel := make(chan int, 100) // create director - director := ui.NewDirector(roomID, imageChannel, audioChannel, inputChannel) + director := emulator.NewDirector(roomID, imageChannel, audioChannel, inputChannel) room := &Room{ imageChannel: imageChannel, @@ -375,9 +374,9 @@ func (r *Room) startVideo() { func (r *Room) startAudio() { log.Println("Enter fan audio") - enc, err := opus.NewEncoder(ui.SampleRate, ui.Channels, opus.AppAudio) + enc, err := opus.NewEncoder(emulator.SampleRate, emulator.Channels, opus.AppAudio) - maxBufferSize := ui.TimeFrame * ui.SampleRate / 1000 + maxBufferSize := emulator.TimeFrame * emulator.SampleRate / 1000 pcm := make([]float32, maxBufferSize) // 640 * 1000 / 16000 == 40 ms idx := 0 @@ -460,7 +459,7 @@ func startWebRTCSession(room *Room, webRTC *webrtc.WebRTC, playerIndex int) { // the first 8 bits belong to player 1 // the next 8 belongs to player 2 ... // We standardize and put it to inputChannel (16 bits) - input = input << ((uint(playerIndex) - 1) * ui.NumKeys) + input = input << ((uint(playerIndex) - 1) * emulator.NumKeys) inputChannel <- input } } diff --git a/config/config.go b/config/config.go index 3f03b0d4..99b9382d 100644 --- a/config/config.go +++ b/config/config.go @@ -8,3 +8,5 @@ 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") +var Width = 256 +var Height = 240 diff --git a/ws/ws.go b/cws/cws.go similarity index 90% rename from ws/ws.go rename to cws/cws.go index 13e66a12..c9327c88 100644 --- a/ws/ws.go +++ b/cws/cws.go @@ -1,4 +1,4 @@ -package main +package cws import ( "encoding/json" @@ -47,8 +47,8 @@ func NewClient(conn *websocket.Conn) *Client { } } -// send sends a packet and trigger callback when the packet comes back -func (c *Client) send(request WSPacket, callback func(response WSPacket)) { +// 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 { @@ -72,8 +72,8 @@ func (c *Client) send(request WSPacket, callback func(response WSPacket)) { c.sendCallbackLock.Unlock() } -// receive receive and response back -func (c *Client) receive(id string, f func(response WSPacket) (request WSPacket)) { +// 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 @@ -101,7 +101,7 @@ func (c *Client) syncSend(request WSPacket) (response WSPacket) { f := func(resp WSPacket) { res <- resp } - c.send(request, f) + c.Send(request, f) return <-res } @@ -111,11 +111,11 @@ func (c *Client) heartbeat() { timer := time.Tick(time.Second) for range timer { - c.send(WSPacket{ID: "heartbeat"}, nil) + c.Send(WSPacket{ID: "heartbeat"}, nil) } } -func (c *Client) listen() { +func (c *Client) Listen() { for { //log.Println("Waiting for message ...") _, rawMsg, err := c.conn.ReadMessage() diff --git a/ui/director.go b/emulator/director.go similarity index 96% rename from ui/director.go rename to emulator/director.go index 349b3652..397e1bcc 100644 --- a/ui/director.go +++ b/emulator/director.go @@ -1,11 +1,11 @@ -package ui +package director import ( "image" "log" "time" - "github.com/giongto35/cloud-game/nes" + "github.com/giongto35/cloud-game/emulator/nes" // "github.com/gordonklaus/portaudio" ) @@ -77,7 +77,7 @@ func (d *Director) Run() { c := time.Tick(time.Second / FPS) L: for range c { - // for { + // for { // quit game // TODO: Anyway not using select because it will slow down select { diff --git a/ui/font.go b/emulator/font.go similarity index 99% rename from ui/font.go rename to emulator/font.go index 882b3a68..ca60ccf9 100644 --- a/ui/font.go +++ b/emulator/font.go @@ -1,5 +1,5 @@ // credit to https://github.com/fogleman/nes -package ui +package director import ( "bytes" diff --git a/ui/gameview.go b/emulator/gameview.go similarity index 96% rename from ui/gameview.go rename to emulator/gameview.go index dd6740f5..e147a9ec 100644 --- a/ui/gameview.go +++ b/emulator/gameview.go @@ -1,10 +1,10 @@ // credit to https://github.com/fogleman/nes -package ui +package director import ( "image" - "github.com/giongto35/cloud-game/nes" + "github.com/giongto35/cloud-game/emulator/nes" ) // List key pressed @@ -32,11 +32,10 @@ const NumKeys = 8 // Audio consts const ( SampleRate = 16000 - Channels = 1 - TimeFrame = 60 + Channels = 1 + TimeFrame = 60 ) - type GameView struct { console *nes.Console title string @@ -53,7 +52,6 @@ type GameView struct { inputChannel chan int } - func NewGameView(console *nes.Console, title, hash string, imageChannel chan *image.RGBA, audioChannel chan float32, inputChannel chan int) *GameView { gameview := &GameView{ console: console, @@ -61,7 +59,7 @@ func NewGameView(console *nes.Console, title, hash string, imageChannel chan *im hash: hash, keyPressed: [NumKeys * 2]bool{false}, imageChannel: imageChannel, - audioChannel: audioChannel, + audioChannel: audioChannel, inputChannel: inputChannel, } diff --git a/ui/nes/apu.go b/emulator/nes/apu.go similarity index 100% rename from ui/nes/apu.go rename to emulator/nes/apu.go diff --git a/ui/nes/cartridge.go b/emulator/nes/cartridge.go similarity index 100% rename from ui/nes/cartridge.go rename to emulator/nes/cartridge.go diff --git a/ui/nes/console.go b/emulator/nes/console.go similarity index 100% rename from ui/nes/console.go rename to emulator/nes/console.go diff --git a/ui/nes/controller.go b/emulator/nes/controller.go similarity index 100% rename from ui/nes/controller.go rename to emulator/nes/controller.go diff --git a/ui/nes/cpu.go b/emulator/nes/cpu.go similarity index 100% rename from ui/nes/cpu.go rename to emulator/nes/cpu.go diff --git a/ui/nes/filter.go b/emulator/nes/filter.go similarity index 100% rename from ui/nes/filter.go rename to emulator/nes/filter.go diff --git a/ui/nes/ines.go b/emulator/nes/ines.go similarity index 100% rename from ui/nes/ines.go rename to emulator/nes/ines.go diff --git a/ui/nes/mapper.go b/emulator/nes/mapper.go similarity index 100% rename from ui/nes/mapper.go rename to emulator/nes/mapper.go diff --git a/ui/nes/mapper1.go b/emulator/nes/mapper1.go similarity index 100% rename from ui/nes/mapper1.go rename to emulator/nes/mapper1.go diff --git a/ui/nes/mapper2.go b/emulator/nes/mapper2.go similarity index 100% rename from ui/nes/mapper2.go rename to emulator/nes/mapper2.go diff --git a/ui/nes/mapper225.go b/emulator/nes/mapper225.go similarity index 100% rename from ui/nes/mapper225.go rename to emulator/nes/mapper225.go diff --git a/ui/nes/mapper3.go b/emulator/nes/mapper3.go similarity index 100% rename from ui/nes/mapper3.go rename to emulator/nes/mapper3.go diff --git a/ui/nes/mapper4.go b/emulator/nes/mapper4.go similarity index 100% rename from ui/nes/mapper4.go rename to emulator/nes/mapper4.go diff --git a/ui/nes/mapper7.go b/emulator/nes/mapper7.go similarity index 100% rename from ui/nes/mapper7.go rename to emulator/nes/mapper7.go diff --git a/ui/nes/memory.go b/emulator/nes/memory.go similarity index 100% rename from ui/nes/memory.go rename to emulator/nes/memory.go diff --git a/ui/nes/palette.go b/emulator/nes/palette.go similarity index 100% rename from ui/nes/palette.go rename to emulator/nes/palette.go diff --git a/ui/nes/ppu.go b/emulator/nes/ppu.go similarity index 100% rename from ui/nes/ppu.go rename to emulator/nes/ppu.go diff --git a/ui/util.go b/emulator/util.go similarity index 97% rename from ui/util.go rename to emulator/util.go index 931ce8d8..c407308d 100644 --- a/ui/util.go +++ b/emulator/util.go @@ -1,5 +1,5 @@ // credit to https://github.com/fogleman/nes -package ui +package director import ( "crypto/md5" @@ -16,7 +16,7 @@ import ( "os/user" "path" - "github.com/giongto35/cloud-game/nes" + "github.com/giongto35/cloud-game/emulator/nes" ) var homeDir string diff --git a/handlers/client/browser.go b/handlers/client/browser.go new file mode 100644 index 00000000..098f45cd --- /dev/null +++ b/handlers/client/browser.go @@ -0,0 +1,136 @@ +package client + +import ( + "encoding/json" + "log" + + "github.com/giongto35/cloud-game/config" + "github.com/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/handlers/gamelist" + "github.com/giongto35/cloud-game/webrtc" + "github.com/gorilla/websocket" + uuid "github.com/satori/go.uuid" +) + +var rooms = map[string]*Room{} + +// NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server +func NewBrowserClient(c *websocket.Conn) *cws.Client { + roomID := "" + // Create connection to overlord + client := cws.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.Send(cws.WSPacket{ + ID: "gamelist", + Data: gamelist.GetEncodedGameList(), + }, nil) + + client.Receive("heartbeat", func(resp cws.WSPacket) cws.WSPacket { + return resp + }) + + client.Receive("initwebrtc", func(resp cws.WSPacket) cws.WSPacket { + log.Println("Received user SDP") + localSession, err := wssession.peerconnection.StartClient(resp.Data, config.Width, config.Height) + if err != nil { + log.Fatalln(err) + } + + return cws.WSPacket{ + ID: "sdp", + Data: localSession, + SessionID: sessionID, + } + }) + + client.Receive("save", func(resp cws.WSPacket) (req cws.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 cws.WSPacket) (req cws.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 + }) + + return client +} diff --git a/handlers/client/overlord.go b/handlers/client/overlord.go new file mode 100644 index 00000000..65ceda36 --- /dev/null +++ b/handlers/client/overlord.go @@ -0,0 +1,78 @@ +package client + +import ( + "log" + + "github.com/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/webrtc" + "github.com/gorilla/websocket" +) + +// NewOverlordClient returns a client connecting to overlord for coordiation between different server +func NewOverlordClient(oc *websocket.Conn) *cws.Client { + 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] + log.Println("start session") + roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) + log.Println("Done, sending back") + // 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 +} diff --git a/handlers/client/session.go b/handlers/client/session.go new file mode 100644 index 00000000..91012c09 --- /dev/null +++ b/handlers/client/session.go @@ -0,0 +1,37 @@ +package client + +import ( + "github.com/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/webrtc" +) + +// Session represents a session connected from the browser to the current server +// It involves one connection to browser and one connection to the overlord +// Peerconnection can be from other server to ensure better latency +type Session struct { + client *cws.Client + peerconnection *webrtc.WebRTC + ServerID string +} + +// startSession handles one session call +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) + room := rooms[roomID] + + webRTC.AttachRoomID(roomID) + go startWebRTCSession(room, webRTC, playerIndex) + + return roomID, isNewRoom +} diff --git a/handlers/games.go b/handlers/gamelist/games.go similarity index 68% rename from handlers/games.go rename to handlers/gamelist/games.go index 8c8923ad..6d0cdf4a 100644 --- a/handlers/games.go +++ b/handlers/gamelist/games.go @@ -1,4 +1,4 @@ -package main +package gamelist import ( "encoding/json" @@ -9,7 +9,7 @@ import ( const gamePath = "games" // getGameList returns list of games stored in games -func getGameList() []string { +func GetGameList() []string { var games []string filepath.Walk(gamePath, func(path string, info os.FileInfo, err error) error { if !info.IsDir() { @@ -24,7 +24,8 @@ func getGameList() []string { return games } -func getEncodedGameList() string { - encodedList, _ := json.Marshal(getGameList()) +// GetEncodedGameList returns game list in encoded wspacket format +func GetEncodedGameList() string { + encodedList, _ := json.Marshal(GetGameList()) return string(encodedList) } diff --git a/handlers/handlers.go b/handlers/handlers.go index 90af04db..fe7b8f4c 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -1,37 +1,42 @@ package handler import ( - "encoding/json" - "image" "io/ioutil" "log" - "math/rand" "net/http" - "strconv" - "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/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/handlers/client" "github.com/gorilla/websocket" - uuid "github.com/satori/go.uuid" - "gopkg.in/hraban/opus.v2" ) +const ( + width = 256 + height = 240 + scale = 3 + title = "NES" + gameboyIndex = "./static/gameboy.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 +// Flag to determine if the server is overlord or not var IsOverlord = false var upgrader = websocket.Upgrader{} // ID to peerconnection -var peerconnections = map[string]*webrtc.WebRTC{} +//var peerconnections = map[string]*webrtc.WebRTC{} var serverID = "" -var oclient *Client +var oclient *cws.Client +// getWeb returns web frontend func getWeb(w http.ResponseWriter, r *http.Request) { bs, err := ioutil.ReadFile(indexFN) if err != nil { @@ -40,84 +45,6 @@ func getWeb(w http.ResponseWriter, r *http.Request) { w.Write(bs) } -// init initilizes a room returns roomID -func initRoom(roomID, gameName string) string { - // if no roomID is given, generate it - if roomID == "" { - roomID = generateRoomID() - } - log.Println("Init new room", roomID, gameName) - imageChannel := make(chan *image.RGBA, 100) - audioChannel := make(chan float32, ui.SampleRate) - inputChannel := make(chan int, 100) - - // create director - director := ui.NewDirector(roomID, imageChannel, audioChannel, inputChannel) - - 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.startVideo() - go room.startAudio() - go director.Start([]string{"games/" + gameName}) - - return roomID -} - -// isRoomRunning check if there is any running sessions. -// TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. -func isRoomRunning(roomID string) bool { - // If no roomID is registered - if _, ok := rooms[roomID]; !ok { - return false - } - - // If there is running session - for _, s := range rooms[roomID].rtcSessions { - if !s.IsClosed() { - return true - } - } - return false -} - -// startSession handles one session call -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) - room := rooms[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) @@ -126,287 +53,133 @@ func ws(w http.ResponseWriter, r *http.Request) { return } defer c.Close() - var gameName string - var roomID string - var playerIndex int + //var gameName string + //var roomID string + //var playerIndex int - // Create connection to overlord - client := NewClient(c) - //sessionID := strconv.Itoa(rand.Int()) - sessionID := uuid.Must(uuid.NewV4()).String() + //// 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 - } + //wssession := &Session{ + //client: client, + //peerconnection: webrtc.NewWebRTC(), + //// The server session is maintaining + //} - client.send(WSPacket{ - ID: "gamelist", - Data: getEncodedGameList(), - }, nil) + //client.send(WSPacket{ + //ID: "gamelist", + //Data: getEncodedGameList(), + //}, nil) - client.receive("heartbeat", func(resp WSPacket) WSPacket { - return resp - }) + //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) - } + //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, - } - }) + //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" - } + //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 - }) + //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" - } + //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 - }) + //return req + //}) - client.receive("start", func(resp WSPacket) (req WSPacket) { - gameName = resp.Data - roomID = resp.RoomID - playerIndex = resp.PlayerIndex - isNewRoom := false + //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 - } - } + //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 + //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 - }) + //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" + //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 - }) + //return req + //}) + client := client.NewBrowserClient(c) 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 -} - -func (r *Room) startVideo() { - // fanout Screen - for { - select { - case <-r.Done: - r.remove() - return - case image := <-r.imageChannel: - //isRoomRunning := false - - 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.ImageChannel <- yuv - } - //isRoomRunning = true - } - r.sessionsLock.Unlock() - } - } -} - -func (r *Room) startAudio() { - 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 - - // fanout Audio - for { - select { - case <-r.Done: - r.remove() - return - case sample := <-r.audioChannel: - pcm[idx] = sample - idx++ - 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) - - r.sessionsLock.Lock() - for _, webRTC := range r.rtcSessions { - // Client stopped - if webRTC.IsClosed() { - continue - } - - // encode frame - // fanout audioChannel - if webRTC.IsConnected() { - // NOTE: can block here - webRTC.AudioChannel <- data - } - //isRoomRunning = true - } - r.sessionsLock.Unlock() - - idx = 0 - count = (count + 1) & 0xff - - } - } - } -} - -func (r *Room) remove() { - log.Println("Closing room", r) - r.director.Done <- struct{}{} -} - -// 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 - } - - // encode frame - if webRTC.IsConnected() { - input := <-webRTC.InputChannel - // the first 8 bits belong to player 1 - // the next 8 belongs to player 2 ... - // We standardize and put it to inputChannel (16 bits) - input = input << ((uint(playerIndex) - 1) * ui.NumKeys) - inputChannel <- input - } - } -} - -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{ + cws.WSPacket{ ID: "getRoom", Data: roomID, }, @@ -422,7 +195,7 @@ func bridgeConnection(session *Session, serverID string, gameName string, roomID // Ask client to init log.Println("Requesting offer to browser", serverID) - resp := client.syncSend(WSPacket{ + resp := client.syncSend(cws.WSPacket{ ID: "requestOffer", Data: "", }) @@ -437,13 +210,13 @@ func bridgeConnection(session *Session, serverID string, gameName string, roomID //ID: "sdp", //Data: remoteTargetSDP.Data, //}) - client.send(WSPacket{ + client.send(cws.WSPacket{ ID: "sdp", Data: remoteTargetSDP.Data, }, nil) log.Println("Init session done, start game on target host") - oclient.syncSend(WSPacket{ + oclient.syncSend(cws.WSPacket{ ID: "start", Data: gameName, TargetHostID: serverID, @@ -461,71 +234,3 @@ func createOverlordConnection() (*websocket.Conn, error) { return c, nil } - -func NewOverlordClient(oc *websocket.Conn) *Client { - 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] - log.Println("start session") - roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) - log.Println("Done, sending back") - // 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 -} diff --git a/handlers/media.go b/handlers/media.go new file mode 100644 index 00000000..ff2bd60b --- /dev/null +++ b/handlers/media.go @@ -0,0 +1,101 @@ +package handler + +import ( + "log" + + "github.com/giongto35/cloud-game/util" + "gopkg.in/hraban/opus.v2" +) + +func (r *Room) startAudio() { + log.Println("Enter fan audio") + + enc, err := opus.NewEncoder(emulator.SampleRate, emulator.Channels, emulator.AppAudio) + + maxBufferSize := emulator.TimeFrame * emulator.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 + + // fanout Audio + for { + select { + case <-r.Done: + r.remove() + return + case sample := <-r.audioChannel: + pcm[idx] = sample + idx++ + 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) + + r.sessionsLock.Lock() + for _, webRTC := range r.rtcSessions { + // Client stopped + if webRTC.IsClosed() { + continue + } + + // encode frame + // fanout audioChannel + if webRTC.IsConnected() { + // NOTE: can block here + webRTC.AudioChannel <- data + } + //isRoomRunning = true + } + r.sessionsLock.Unlock() + + idx = 0 + count = (count + 1) & 0xff + + } + } + } +} + +func (r *Room) startVideo() { + // fanout Screen + for { + select { + case <-r.Done: + r.remove() + return + case image := <-r.imageChannel: + //isRoomRunning := false + + 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.ImageChannel <- yuv + } + //isRoomRunning = true + } + r.sessionsLock.Unlock() + } + } +} diff --git a/handlers/rooms.go b/handlers/rooms.go index 725cf94f..18f2e2b5 100644 --- a/handlers/rooms.go +++ b/handlers/rooms.go @@ -3,9 +3,11 @@ package handler import ( "image" "log" + "math/rand" + "strconv" "sync" - "github.com/giongto35/cloud-game/ui" + ui "github.com/giongto35/cloud-game/emulator" "github.com/giongto35/cloud-game/webrtc" ) @@ -26,6 +28,13 @@ type Room struct { var rooms = map[string]*Room{} +// 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 +} + // init initilizes a room returns roomID func initRoom(roomID, gameName string) string { // if no roomID is given, generate it @@ -74,3 +83,58 @@ func isRoomRunning(roomID string) bool { } return false } + +// 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 + } + + // encode frame + if webRTC.IsConnected() { + input := <-webRTC.InputChannel + // the first 8 bits belong to player 1 + // the next 8 belongs to player 2 ... + // We standardize and put it to inputChannel (16 bits) + input = input << ((uint(playerIndex) - 1) * ui.NumKeys) + inputChannel <- input + } + } +} + +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 (r *Room) remove() { + log.Println("Closing room", r) + r.director.Done <- struct{}{} +} From 74b66ae6eabbdfc0e76cc47432f3f848c6ebab45 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Thanh Date: Tue, 30 Apr 2019 17:33:35 +0700 Subject: [PATCH 04/23] WIP --- cmd/main.go | 546 +----------------------- cws/cws.go | 8 +- emulator/director.go | 2 +- emulator/font.go | 2 +- emulator/gameview.go | 3 +- emulator/util.go | 2 +- handler/browser.go | 156 +++++++ {handlers => handler}/gamelist/games.go | 0 {handlers => handler}/handlers.go | 88 ++-- {handlers => handler}/media.go | 3 +- handler/overlord.go | 138 ++++++ {handlers => handler}/rooms.go | 10 +- {handlers/client => handler}/session.go | 6 +- handlers/client/browser.go | 136 ------ handlers/client/overlord.go | 78 ---- handlers/overlord.go | 86 ---- overlord/overlord.go | 9 +- 17 files changed, 351 insertions(+), 922 deletions(-) create mode 100644 handler/browser.go rename {handlers => handler}/gamelist/games.go (100%) rename {handlers => handler}/handlers.go (69%) rename {handlers => handler}/media.go (96%) create mode 100644 handler/overlord.go rename {handlers => handler}/rooms.go (91%) rename {handlers/client => handler}/session.go (92%) delete mode 100644 handlers/client/browser.go delete mode 100644 handlers/client/overlord.go delete mode 100644 handlers/overlord.go diff --git a/cmd/main.go b/cmd/main.go index 78f8de44..509e999d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,34 +1,21 @@ package main import ( - "encoding/json" "flag" - "image" - "io/ioutil" "log" "math/rand" "net/http" _ "net/http/pprof" - "strconv" "strings" - "sync" "time" "github.com/giongto35/cloud-game/config" - "github.com/giongto35/cloud-game/util" - "github.com/giongto35/cloud-game/webrtc" + "github.com/giongto35/cloud-game/handler" + "github.com/giongto35/cloud-game/overlord" "github.com/gorilla/websocket" - pionRTC "github.com/pion/webrtc" - uuid "github.com/satori/go.uuid" - - "gopkg.in/hraban/opus.v2" ) const ( - width = 256 - height = 240 - scale = 3 - title = "NES" gameboyIndex = "./static/gameboy.html" debugIndex = "./static/index_ws.html" ) @@ -42,28 +29,6 @@ var writeWait = 30 * time.Second var IsOverlord = false var upgrader = websocket.Upgrader{} -// 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 - audioChannel chan float32 - inputChannel chan int - // Done channel is to fire exit event when there is no webRTC session running - Done chan struct{} - - rtcSessions []*webrtc.WebRTC - sessionsLock *sync.Mutex - - director *emulator.Director -} - -var rooms = map[string]*Room{} - -// ID to peerconnection -var peerconnections = map[string]*webrtc.WebRTC{} -var serverID = "" -var oclient *Client - func main() { flag.Parse() log.Println("Usage: ./game [debug]") @@ -84,15 +49,15 @@ func main() { IsOverlord = false } + handler, err := handler.NewHandler(IsOverlord) rand.Seed(time.Now().UTC().UnixNano()) - rooms = map[string]*Room{} // ignore origin upgrader.CheckOrigin = func(r *http.Request) bool { return true } - http.HandleFunc("/", getWeb) + http.HandleFunc("/", handler.GetWeb) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) - http.HandleFunc("/ws", ws) + http.HandleFunc("/ws", handler.WS) if !IsOverlord { conn, err := createOverlordConnection() @@ -105,512 +70,13 @@ func main() { } } - 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.HandleFunc("/wso", overlord.WSO) http.ListenAndServe(":9000", nil) } } - -func getWeb(w http.ResponseWriter, r *http.Request) { - bs, err := ioutil.ReadFile(indexFN) - if err != nil { - log.Fatal(err) - } - w.Write(bs) -} - -// init initilizes a room returns roomID -func initRoom(roomID, gameName string) string { - // if no roomID is given, generate it - if roomID == "" { - roomID = generateRoomID() - } - log.Println("Init new room", roomID, gameName) - imageChannel := make(chan *image.RGBA, 100) - audioChannel := make(chan float32, emulator.SampleRate) - inputChannel := make(chan int, 100) - - // create director - director := emulator.NewDirector(roomID, imageChannel, audioChannel, inputChannel) - - 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.startVideo() - go room.startAudio() - go director.Start([]string{"games/" + gameName}) - - return roomID -} - -// isRoomRunning check if there is any running sessions. -// TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. -func isRoomRunning(roomID string) bool { - // If no roomID is registered - if _, ok := rooms[roomID]; !ok { - return false - } - - // If there is running session - for _, s := range rooms[roomID].rtcSessions { - if !s.IsClosed() { - return true - } - } - return false -} - -// startSession handles one session call -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) - room := rooms[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 { - log.Print("[!] WS upgrade:", err) - return - } - defer c.Close() - var gameName string - var roomID string - var playerIndex int - - // 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.send(WSPacket{ - ID: "gamelist", - Data: getEncodedGameList(), - }, nil) - - 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 -} - -func (r *Room) startVideo() { - // fanout Screen - for { - select { - case <-r.Done: - r.remove() - return - case image := <-r.imageChannel: - //isRoomRunning := false - - 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.ImageChannel <- yuv - } - //isRoomRunning = true - } - r.sessionsLock.Unlock() - } - } -} - -func (r *Room) startAudio() { - log.Println("Enter fan audio") - - enc, err := opus.NewEncoder(emulator.SampleRate, emulator.Channels, opus.AppAudio) - - maxBufferSize := emulator.TimeFrame * emulator.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 - - // fanout Audio - for { - select { - case <-r.Done: - r.remove() - return - case sample := <-r.audioChannel: - pcm[idx] = sample - idx++ - 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) - - r.sessionsLock.Lock() - for _, webRTC := range r.rtcSessions { - // Client stopped - if webRTC.IsClosed() { - continue - } - - // encode frame - // fanout audioChannel - if webRTC.IsConnected() { - // NOTE: can block here - webRTC.AudioChannel <- data - } - //isRoomRunning = true - } - r.sessionsLock.Unlock() - - idx = 0 - count = (count + 1) & 0xff - - } - } - } -} - -func (r *Room) remove() { - log.Println("Closing room", r) - r.director.Done <- struct{}{} -} - -// 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 - } - - // encode frame - if webRTC.IsConnected() { - input := <-webRTC.InputChannel - // the first 8 bits belong to player 1 - // the next 8 belongs to player 2 ... - // We standardize and put it to inputChannel (16 bits) - input = input << ((uint(playerIndex) - 1) * emulator.NumKeys) - inputChannel <- input - } - } -} - -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(oc *websocket.Conn) *Client { - 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] - log.Println("start session") - roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) - log.Println("Done, sending back") - // 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 -} diff --git a/cws/cws.go b/cws/cws.go index c9327c88..b943ad55 100644 --- a/cws/cws.go +++ b/cws/cws.go @@ -95,8 +95,8 @@ func (c *Client) Receive(id string, f func(response WSPacket) (request WSPacket) } } -// syncSend sends a packet and wait for callback till the packet comes back -func (c *Client) syncSend(request WSPacket) (response WSPacket) { +// 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 @@ -105,8 +105,8 @@ func (c *Client) syncSend(request WSPacket) (response WSPacket) { return <-res } -// heartbeat maintains connection to server -func (c *Client) heartbeat() { +// Heartbeat maintains connection to server +func (c *Client) Heartbeat() { // send heartbeat every 1s timer := time.Tick(time.Second) diff --git a/emulator/director.go b/emulator/director.go index 397e1bcc..ce3a3ea4 100644 --- a/emulator/director.go +++ b/emulator/director.go @@ -1,4 +1,4 @@ -package director +package emulator import ( "image" diff --git a/emulator/font.go b/emulator/font.go index ca60ccf9..e41c7448 100644 --- a/emulator/font.go +++ b/emulator/font.go @@ -1,5 +1,5 @@ // credit to https://github.com/fogleman/nes -package director +package emulator import ( "bytes" diff --git a/emulator/gameview.go b/emulator/gameview.go index e147a9ec..de429f2b 100644 --- a/emulator/gameview.go +++ b/emulator/gameview.go @@ -1,5 +1,5 @@ // credit to https://github.com/fogleman/nes -package director +package emulator import ( "image" @@ -34,6 +34,7 @@ const ( SampleRate = 16000 Channels = 1 TimeFrame = 60 + AppAudio = 1 ) type GameView struct { diff --git a/emulator/util.go b/emulator/util.go index c407308d..79cbec16 100644 --- a/emulator/util.go +++ b/emulator/util.go @@ -1,5 +1,5 @@ // credit to https://github.com/fogleman/nes -package director +package emulator import ( "crypto/md5" diff --git a/handler/browser.go b/handler/browser.go new file mode 100644 index 00000000..8425a4f0 --- /dev/null +++ b/handler/browser.go @@ -0,0 +1,156 @@ +package handler + +import ( + "encoding/json" + "log" + + "github.com/giongto35/cloud-game/config" + "github.com/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/handler/gamelist" + "github.com/giongto35/cloud-game/webrtc" + "github.com/gorilla/websocket" + pionRTC "github.com/pion/webrtc" + uuid "github.com/satori/go.uuid" +) + +type BrowserClient struct { + *cws.Client + session *Session + oclient *OverlordClient + gameName string + roomID string + playerIndex int +} + +// NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server +func NewBrowserClient(c *websocket.Conn, overlordClient *OverlordClient) *BrowserClient { + roomID := "" + gameName := "" + playerIndex := 0 + // Create connection to overlord + browserClient := &BrowserClient{ + Client: cws.NewClient(c), + gameName: "", + roomID: "", + playerIndex: 0, + } + + //sessionID := strconv.Itoa(rand.Int()) + sessionID := uuid.Must(uuid.NewV4()).String() + + wssession := &Session{ + BrowserClient: browserClient, + OverlordClient: overlordClient, + peerconnection: webrtc.NewWebRTC(), + // The server session is maintaining + } + + browserClient.Send(cws.WSPacket{ + ID: "gamelist", + Data: gamelist.GetEncodedGameList(), + }, nil) + + browserClient.Receive("heartbeat", func(resp cws.WSPacket) cws.WSPacket { + return resp + }) + + browserClient.Receive("initwebrtc", func(resp cws.WSPacket) cws.WSPacket { + log.Println("Received user SDP") + localSession, err := wssession.peerconnection.StartClient(resp.Data, config.Width, config.Height) + if err != nil { + log.Fatalln(err) + } + + return cws.WSPacket{ + ID: "sdp", + Data: localSession, + SessionID: sessionID, + } + }) + + // TODO: Add save and load + //browserClient.Receive("save", func(resp cws.WSPacket) (req cws.WSPacket) { + //log.Println("Saving game state") + //req.ID = "save" + //req.Data = "ok" + //if roomID != "" { + ////err := rooms[roomID].director.SaveGame() + //err := browserClient.room.director.SaveGame() + //if err != nil { + //log.Println("[!] Cannot save game state: ", err) + //req.Data = "error" + //} + //} else { + //req.Data = "error" + //} + + //return req + //}) + + //browserClient.Receive("load", func(resp cws.WSPacket) (req cws.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 + //}) + + browserClient.Receive("start", func(resp cws.WSPacket) (req cws.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 browserClient.oclient != nil { + session := browserClient.session + roomServerID := getServerIDOfRoom(session.OverlordClient, 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 && browserClient.session.OverlordClient != nil { + browserClient.session.OverlordClient.Send(cws.WSPacket{ + ID: "registerRoom", + Data: roomID, + }, nil) + } + req.ID = "start" + req.RoomID = roomID + req.SessionID = sessionID + + return req + }) + + browserClient.Receive("candidate", func(resp cws.WSPacket) (req cws.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 + }) + + return browserClient +} diff --git a/handlers/gamelist/games.go b/handler/gamelist/games.go similarity index 100% rename from handlers/gamelist/games.go rename to handler/gamelist/games.go diff --git a/handlers/handlers.go b/handler/handlers.go similarity index 69% rename from handlers/handlers.go rename to handler/handlers.go index fe7b8f4c..47ac6dc9 100644 --- a/handlers/handlers.go +++ b/handler/handlers.go @@ -7,14 +7,11 @@ import ( "time" "github.com/giongto35/cloud-game/config" - "github.com/giongto35/cloud-game/cws" - "github.com/giongto35/cloud-game/handlers/client" + "github.com/giongto35/cloud-game/webrtc" "github.com/gorilla/websocket" ) const ( - width = 256 - height = 240 scale = 3 title = "NES" gameboyIndex = "./static/gameboy.html" @@ -33,11 +30,30 @@ var upgrader = websocket.Upgrader{} // ID to peerconnection //var peerconnections = map[string]*webrtc.WebRTC{} -var serverID = "" -var oclient *cws.Client +var oclient *OverlordClient -// getWeb returns web frontend -func getWeb(w http.ResponseWriter, r *http.Request) { +type Handler struct { + oClient *OverlordClient + rooms map[string]*Room + serverID string + // ID to peerconnection + peerconnections map[string]*webrtc.WebRTC +} + +func NewHandler() (*Handler, error) { + conn, err := createOverlordConnection() + if err != nil { + return nil, err + } + return &Handler{ + oClient: NewOverlordClient(conn), + rooms: map[string]*Room{}, + peerconnections: map[string]*webrtc.WebRTC{}, + }, nil +} + +// GetWeb returns web frontend +func (h *Handler) GetWeb(w http.ResponseWriter, r *http.Request) { bs, err := ioutil.ReadFile(indexFN) if err != nil { log.Fatal(err) @@ -46,7 +62,7 @@ func getWeb(w http.ResponseWriter, r *http.Request) { } // Handle normal traffic (from browser to host) -func ws(w http.ResponseWriter, r *http.Request) { +func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Print("[!] WS upgrade:", err) @@ -172,58 +188,8 @@ func ws(w http.ResponseWriter, r *http.Request) { //return req //}) - client := client.NewBrowserClient(c) - client.listen() -} - -func getServerIDOfRoom(oc *Client, roomID string) string { - log.Println("Request overlord roomID") - packet := oc.syncSend( - cws.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(cws.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(cws.WSPacket{ - ID: "sdp", - Data: remoteTargetSDP.Data, - }, nil) - log.Println("Init session done, start game on target host") - - oclient.syncSend(cws.WSPacket{ - ID: "start", - Data: gameName, - TargetHostID: serverID, - RoomID: roomID, - PlayerIndex: playerIndex, - }) - log.Println("Game is started on remote host") + client := NewBrowserClient(c, oclient) + client.Listen() } func createOverlordConnection() (*websocket.Conn, error) { diff --git a/handlers/media.go b/handler/media.go similarity index 96% rename from handlers/media.go rename to handler/media.go index ff2bd60b..244230a7 100644 --- a/handlers/media.go +++ b/handler/media.go @@ -3,6 +3,7 @@ package handler import ( "log" + "github.com/giongto35/cloud-game/emulator" "github.com/giongto35/cloud-game/util" "gopkg.in/hraban/opus.v2" ) @@ -10,7 +11,7 @@ import ( func (r *Room) startAudio() { log.Println("Enter fan audio") - enc, err := opus.NewEncoder(emulator.SampleRate, emulator.Channels, emulator.AppAudio) + enc, err := opus.NewEncoder(emulator.SampleRate, emulator.Channels, opus.AppAudio) maxBufferSize := emulator.TimeFrame * emulator.SampleRate / 1000 pcm := make([]float32, maxBufferSize) // 640 * 1000 / 16000 == 40 ms diff --git a/handler/overlord.go b/handler/overlord.go new file mode 100644 index 00000000..4d2575fe --- /dev/null +++ b/handler/overlord.go @@ -0,0 +1,138 @@ +package handler + +import ( + "log" + + "github.com/giongto35/cloud-game/config" + "github.com/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/webrtc" + "github.com/gorilla/websocket" +) + +type OverlordClient struct { + *cws.Client +} + +// NewOverlordClient returns a client connecting to overlord for coordiation between different server +func NewOverlordClient(oc *websocket.Conn) *OverlordClient { + oclient := &OverlordClient{ + Client: cws.NewClient(oc), + } + return oclient +} + +func (h *Handler) Register(oclient *OverlordClient) { + + // Received from overlord the serverID + oclient.Receive( + "serverID", + func(response cws.WSPacket) (request cws.WSPacket) { + // Stick session with serverID got from overlord + log.Println("Received serverID ", response.Data) + h.serverID = response.Data + + return cws.EmptyPacket + }, + ) + + // Received from overlord the sdp. This is happens when bridging + // TODO: refactor + oclient.Receive( + "initwebrtc", + func(resp cws.WSPacket) (req cws.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, config.Width, config.Height) + h.peerconnections[resp.SessionID] = peerconnection + + if err != nil { + log.Fatalln(err) + } + + return cws.WSPacket{ + ID: "sdp", + Data: localSession, + } + }, + ) + + // Received start from overlord. This is happens when bridging + // TODO: refactor + oclient.Receive( + "start", + func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Received a start request from overlord") + log.Println("Add the connection to current room on the host") + + peerconnection := h.peerconnections[resp.SessionID] + log.Println("start session") + roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) + log.Println("Done, sending back") + // 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() + +} + +func getServerIDOfRoom(oc *OverlordClient, roomID string) string { + log.Println("Request overlord roomID") + packet := oc.SyncSend( + cws.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.BrowserClient + // Ask client to init + + log.Println("Requesting offer to browser", serverID) + resp := client.SyncSend(cws.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 := session.OverlordClient.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, + //}) + session.BrowserClient.Send(cws.WSPacket{ + ID: "sdp", + Data: remoteTargetSDP.Data, + }, nil) + log.Println("Init session done, start game on target host") + + session.OverlordClient.SyncSend(cws.WSPacket{ + ID: "start", + Data: gameName, + TargetHostID: serverID, + RoomID: roomID, + PlayerIndex: playerIndex, + }) + log.Println("Game is started on remote host") +} diff --git a/handlers/rooms.go b/handler/rooms.go similarity index 91% rename from handlers/rooms.go rename to handler/rooms.go index 18f2e2b5..393da503 100644 --- a/handlers/rooms.go +++ b/handler/rooms.go @@ -7,7 +7,7 @@ import ( "strconv" "sync" - ui "github.com/giongto35/cloud-game/emulator" + emulator "github.com/giongto35/cloud-game/emulator" "github.com/giongto35/cloud-game/webrtc" ) @@ -23,7 +23,7 @@ type Room struct { rtcSessions []*webrtc.WebRTC sessionsLock *sync.Mutex - director *ui.Director + director *emulator.Director } var rooms = map[string]*Room{} @@ -43,11 +43,11 @@ func initRoom(roomID, gameName string) string { } log.Println("Init new room", roomID, gameName) imageChannel := make(chan *image.RGBA, 100) - audioChannel := make(chan float32, ui.SampleRate) + audioChannel := make(chan float32, emulator.SampleRate) inputChannel := make(chan int, 100) // create director - director := ui.NewDirector(roomID, imageChannel, audioChannel, inputChannel) + director := emulator.NewDirector(roomID, imageChannel, audioChannel, inputChannel) room := &Room{ imageChannel: imageChannel, @@ -105,7 +105,7 @@ func startWebRTCSession(room *Room, webRTC *webrtc.WebRTC, playerIndex int) { // the first 8 bits belong to player 1 // the next 8 belongs to player 2 ... // We standardize and put it to inputChannel (16 bits) - input = input << ((uint(playerIndex) - 1) * ui.NumKeys) + input = input << ((uint(playerIndex) - 1) * emulator.NumKeys) inputChannel <- input } } diff --git a/handlers/client/session.go b/handler/session.go similarity index 92% rename from handlers/client/session.go rename to handler/session.go index 91012c09..5bd13729 100644 --- a/handlers/client/session.go +++ b/handler/session.go @@ -1,7 +1,6 @@ -package client +package handler import ( - "github.com/giongto35/cloud-game/cws" "github.com/giongto35/cloud-game/webrtc" ) @@ -9,7 +8,8 @@ import ( // It involves one connection to browser and one connection to the overlord // Peerconnection can be from other server to ensure better latency type Session struct { - client *cws.Client + BrowserClient *BrowserClient + OverlordClient *OverlordClient peerconnection *webrtc.WebRTC ServerID string } diff --git a/handlers/client/browser.go b/handlers/client/browser.go deleted file mode 100644 index 098f45cd..00000000 --- a/handlers/client/browser.go +++ /dev/null @@ -1,136 +0,0 @@ -package client - -import ( - "encoding/json" - "log" - - "github.com/giongto35/cloud-game/config" - "github.com/giongto35/cloud-game/cws" - "github.com/giongto35/cloud-game/handlers/gamelist" - "github.com/giongto35/cloud-game/webrtc" - "github.com/gorilla/websocket" - uuid "github.com/satori/go.uuid" -) - -var rooms = map[string]*Room{} - -// NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server -func NewBrowserClient(c *websocket.Conn) *cws.Client { - roomID := "" - // Create connection to overlord - client := cws.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.Send(cws.WSPacket{ - ID: "gamelist", - Data: gamelist.GetEncodedGameList(), - }, nil) - - client.Receive("heartbeat", func(resp cws.WSPacket) cws.WSPacket { - return resp - }) - - client.Receive("initwebrtc", func(resp cws.WSPacket) cws.WSPacket { - log.Println("Received user SDP") - localSession, err := wssession.peerconnection.StartClient(resp.Data, config.Width, config.Height) - if err != nil { - log.Fatalln(err) - } - - return cws.WSPacket{ - ID: "sdp", - Data: localSession, - SessionID: sessionID, - } - }) - - client.Receive("save", func(resp cws.WSPacket) (req cws.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 cws.WSPacket) (req cws.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 - }) - - return client -} diff --git a/handlers/client/overlord.go b/handlers/client/overlord.go deleted file mode 100644 index 65ceda36..00000000 --- a/handlers/client/overlord.go +++ /dev/null @@ -1,78 +0,0 @@ -package client - -import ( - "log" - - "github.com/giongto35/cloud-game/cws" - "github.com/giongto35/cloud-game/webrtc" - "github.com/gorilla/websocket" -) - -// NewOverlordClient returns a client connecting to overlord for coordiation between different server -func NewOverlordClient(oc *websocket.Conn) *cws.Client { - 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] - log.Println("start session") - roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) - log.Println("Done, sending back") - // 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 -} diff --git a/handlers/overlord.go b/handlers/overlord.go deleted file mode 100644 index 660d7055..00000000 --- a/handlers/overlord.go +++ /dev/null @@ -1,86 +0,0 @@ -package handler - -import ( - "log" - - "github.com/giongto35/cloud-game/config" - "github.com/giongto35/cloud-game/webrtc" - "github.com/gorilla/websocket" -) - -func createOverlordConnection() (*websocket.Conn, error) { - c, _, err := websocket.DefaultDialer.Dial(*config.OverlordHost, nil) - if err != nil { - return nil, err - } - - return c, nil -} - -func NewOverlordClient(oc *websocket.Conn) *Client { - 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] - log.Println("start session") - roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) - log.Println("Done, sending back") - // 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 -} diff --git a/overlord/overlord.go b/overlord/overlord.go index 4a9795f8..3e67de4a 100644 --- a/overlord/overlord.go +++ b/overlord/overlord.go @@ -1,4 +1,4 @@ -package main +package overlord import ( "fmt" @@ -7,16 +7,17 @@ import ( "net/http" "strconv" + "github.com/giongto35/cloud-game/cws" "github.com/giongto35/cloud-game/webrtc" ) var roomToServer = map[string]string{} // servers are the map serverID to server Client -var servers = map[string]*Client{} +var servers = map[string]*cws.Client{} // If it's overlord, handle overlord connection (from host to overlord) -func wso(w http.ResponseWriter, r *http.Request) { +func WSO(w http.ResponseWriter, r *http.Request) { fmt.Println("Connected") c, err := upgrader.Upgrade(w, r, nil) if err != nil { @@ -119,5 +120,5 @@ func wso(w http.ResponseWriter, r *http.Request) { } }) - client.listen() + client.listeồi } From 7b2b9d034b70e3f4fb79fda7f9a786b23b05b3e4 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Thanh Date: Wed, 1 May 2019 11:02:37 +0800 Subject: [PATCH 05/23] WIP --- cmd/main.go | 74 ++++++++++++++++------------- cmd/main_test.go | 63 ++++++++++++++---------- handler/browser.go | 62 +++++++++++++----------- handler/handlers.go | 21 ++++---- handler/overlord.go | 2 +- overlord/overlord.go | 111 +++++++++++++++++++++++++------------------ 6 files changed, 191 insertions(+), 142 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 509e999d..ad070a5d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,7 +3,6 @@ package main import ( "flag" "log" - "math/rand" "net/http" _ "net/http/pprof" "strings" @@ -29,6 +28,47 @@ var writeWait = 30 * time.Second var IsOverlord = false var upgrader = websocket.Upgrader{} +// initilizeOverlord setup an overlord server +func initilizeOverlord() { + overlord := overlord.NewServer() + + log.Println("http://localhost:9000") + + // Can consider Overlord works as server but it is complicated + http.HandleFunc("/wso", overlord.WSO) + http.ListenAndServe(":9000", nil) +} + +func createOverlordConnection() (*websocket.Conn, error) { + c, _, err := websocket.DefaultDialer.Dial(*config.OverlordHost, nil) + if err != nil { + return nil, err + } + + return c, nil +} + +// initializeServer setup a server +func initializeServer() { + conn, err := createOverlordConnection() + if err != nil { + log.Println("Cannot connect to overlord") + log.Println("Run as a single server") + } + + handler := handler.NewHandler(conn) + + // ignore origin + upgrader.CheckOrigin = func(r *http.Request) bool { return true } + + http.HandleFunc("/", handler.GetWeb) + http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) + http.HandleFunc("/ws", handler.WS) + + log.Println("http://localhost:" + *config.Port) + http.ListenAndServe(":"+*config.Port, nil) +} + func main() { flag.Parse() log.Println("Usage: ./game [debug]") @@ -40,6 +80,7 @@ func main() { if *config.OverlordHost == "overlord" { log.Println("Running as overlord ") + initilizeOverlord() IsOverlord = true } else { if strings.HasPrefix(*config.OverlordHost, "ws") && !strings.HasSuffix(*config.OverlordHost, "wso") { @@ -48,35 +89,4 @@ func main() { log.Println("Running as slave ") IsOverlord = false } - - handler, err := handler.NewHandler(IsOverlord) - rand.Seed(time.Now().UTC().UnixNano()) - - // ignore origin - upgrader.CheckOrigin = func(r *http.Request) bool { return true } - - http.HandleFunc("/", handler.GetWeb) - http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static")))) - http.HandleFunc("/ws", handler.WS) - - if !IsOverlord { - conn, err := createOverlordConnection() - if err != nil { - log.Println("Cannot connect to overlord") - log.Println("Run as a single server") - oclient = nil - } else { - oclient = NewOverlordClient(conn) - } - } - - 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", overlord.WSO) - http.ListenAndServe(":9000", nil) - } } diff --git a/cmd/main_test.go b/cmd/main_test.go index 715d1a42..45e16b07 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -8,6 +8,9 @@ import ( "testing" "time" + "github.com/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/handler" + "github.com/giongto35/cloud-game/overlord" gamertc "github.com/giongto35/cloud-game/webrtc" "github.com/gorilla/websocket" "github.com/pion/webrtc" @@ -17,26 +20,36 @@ var host = "http://localhost:8000" var webrtcconfig = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}} func initOverlord() *httptest.Server { - overlord := httptest.NewServer(http.HandlerFunc(wso)) + server := overlord.NewServer() + overlord := httptest.NewServer(http.HandlerFunc(server.WSO)) return overlord } -func initServer(t *testing.T, overlordURL string) *httptest.Server { - if overlordURL == "" { - oclient = nil - } else { - u := "ws" + strings.TrimPrefix(overlordURL, "http") - fmt.Println("connecting to overlord: ", u) +func initServer(t *testing.T, oclient *handler.OverlordClient) *httptest.Server { + conn := connectTestOverlordServer() + handler, err := handler.NewHandler(oclient) + if err != nil { + t.Fatalf("%v", err) + } + server := httptest.NewServer(http.HandlerFunc(handler.WS)) + return server +} - oconn, _, err := websocket.DefaultDialer.Dial(u, nil) - if err != nil { - t.Fatalf("%v", err) - } - oclient = NewOverlordClient(oconn) +func connectTestOverlordServer(t *testing.T, overlordURL string) *handler.OverlordClient { + if overlordURL == "" { + return nil + } else { + overlordURL = "ws" + strings.TrimPrefix(overlordURL, "http") + fmt.Println("connecting to overlord: ", overlordURL) } - server := httptest.NewServer(http.HandlerFunc(ws)) - return server + oconn, _, err := websocket.DefaultDialer.Dial(overlordURL, nil) + if err != nil { + t.Fatalf("%v", err) + } + defer oconn.Close() + + return oconn } func initClient(t *testing.T, host string) { @@ -69,17 +82,17 @@ func initClient(t *testing.T, host string) { } // Send offer to server - client := NewClient(ws) - go client.listen() + client := cws.NewClient(ws) + go client.Listen() fmt.Println("Sending offer...") - client.send(WSPacket{ + client.Send(cws.WSPacket{ ID: "initwebrtc", Data: gamertc.Encode(offer), }, nil) fmt.Println("Waiting sdp...") - client.receive("sdp", func(resp WSPacket) WSPacket { + client.Receive("sdp", func(resp cws.WSPacket) cws.WSPacket { fmt.Println("received", resp.Data) answer := webrtc.SessionDescription{} gamertc.Decode(resp.Data, &answer) @@ -89,19 +102,19 @@ func initClient(t *testing.T, host string) { panic(err) } - return EmptyPacket + return cws.EmptyPacket }) time.Sleep(time.Second * 3) fmt.Println("Sending start...") roomID := make(chan string) - client.send(WSPacket{ + client.Send(cws.WSPacket{ ID: "start", Data: "Contra.nes", RoomID: "", PlayerIndex: 1, - }, func(resp WSPacket) { + }, func(resp cws.WSPacket) { fmt.Println("Received response") fmt.Println("RoomID:", resp.RoomID) roomID <- resp.RoomID @@ -119,8 +132,7 @@ func initClient(t *testing.T, host string) { func TestSingleServerNoOverlord(t *testing.T) { // Init slave server - oclient = nil - s := initServer(t, "") + s := initServer(t, nil) defer s.Close() initClient(t, s.URL) @@ -129,12 +141,13 @@ func TestSingleServerNoOverlord(t *testing.T) { func TestSingleServerOneOverlord(t *testing.T) { o := initOverlord() defer o.Close() + + oconn := connectOverlord(t, o.URL) // Init slave server - s := initServer(t, o.URL) + s := initServer(t, oconn) defer s.Close() initClient(t, s.URL) - oclient.conn.Close() } //func TestTwoServerOneOverlord(t *testing.T) { diff --git a/handler/browser.go b/handler/browser.go index 8425a4f0..082c22a5 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -22,33 +22,8 @@ type BrowserClient struct { playerIndex int } -// NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server -func NewBrowserClient(c *websocket.Conn, overlordClient *OverlordClient) *BrowserClient { - roomID := "" - gameName := "" - playerIndex := 0 - // Create connection to overlord - browserClient := &BrowserClient{ - Client: cws.NewClient(c), - gameName: "", - roomID: "", - playerIndex: 0, - } - - //sessionID := strconv.Itoa(rand.Int()) - sessionID := uuid.Must(uuid.NewV4()).String() - - wssession := &Session{ - BrowserClient: browserClient, - OverlordClient: overlordClient, - peerconnection: webrtc.NewWebRTC(), - // The server session is maintaining - } - - browserClient.Send(cws.WSPacket{ - ID: "gamelist", - Data: gamelist.GetEncodedGameList(), - }, nil) +func (s *Session) RegisterBrowserClient() { + browserClient := s.BrowserClient browserClient.Receive("heartbeat", func(resp cws.WSPacket) cws.WSPacket { return resp @@ -56,7 +31,7 @@ func NewBrowserClient(c *websocket.Conn, overlordClient *OverlordClient) *Browse browserClient.Receive("initwebrtc", func(resp cws.WSPacket) cws.WSPacket { log.Println("Received user SDP") - localSession, err := wssession.peerconnection.StartClient(resp.Data, config.Width, config.Height) + localSession, err := s.peerconnection.StartClient(resp.Data, config.Width, config.Height) if err != nil { log.Fatalln(err) } @@ -64,7 +39,7 @@ func NewBrowserClient(c *websocket.Conn, overlordClient *OverlordClient) *Browse return cws.WSPacket{ ID: "sdp", Data: localSession, - SessionID: sessionID, + SessionID: s.SessionID, } }) @@ -152,5 +127,34 @@ func NewBrowserClient(c *websocket.Conn, overlordClient *OverlordClient) *Browse return req }) +} + +// NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server +func NewBrowserClient(c *websocket.Conn, overlordClient *OverlordClient) *BrowserClient { + roomID := "" + gameName := "" + playerIndex := 0 + // Create connection to overlord + browserClient := &BrowserClient{ + Client: cws.NewClient(c), + gameName: "", + roomID: "", + playerIndex: 0, + } + + //sessionID := strconv.Itoa(rand.Int()) + sessionID := uuid.Must(uuid.NewV4()).String() + + wssession := &Session{ + BrowserClient: browserClient, + OverlordClient: overlordClient, + peerconnection: webrtc.NewWebRTC(), + // The server session is maintaining + } + + browserClient.Send(cws.WSPacket{ + ID: "gamelist", + Data: gamelist.GetEncodedGameList(), + }, nil) return browserClient } diff --git a/handler/handlers.go b/handler/handlers.go index 47ac6dc9..b9f4ab8c 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -30,7 +30,7 @@ var upgrader = websocket.Upgrader{} // ID to peerconnection //var peerconnections = map[string]*webrtc.WebRTC{} -var oclient *OverlordClient +//var oclient *OverlordClient type Handler struct { oClient *OverlordClient @@ -38,18 +38,21 @@ type Handler struct { serverID string // ID to peerconnection peerconnections map[string]*webrtc.WebRTC + // Session + wssession Session } -func NewHandler() (*Handler, error) { - conn, err := createOverlordConnection() - if err != nil { - return nil, err - } +// NewHandler returns a new server +func NewHandler(overlordConn *websocket.Conn) *Handler { + //conn, err := createOverlordConnection() + //if err != nil { + //return nil, err + //} return &Handler{ - oClient: NewOverlordClient(conn), + oClient: NewOverlordClient(overlordConn), rooms: map[string]*Room{}, peerconnections: map[string]*webrtc.WebRTC{}, - }, nil + } } // GetWeb returns web frontend @@ -61,7 +64,7 @@ func (h *Handler) GetWeb(w http.ResponseWriter, r *http.Request) { w.Write(bs) } -// Handle normal traffic (from browser to host) +// WS handles normal traffic (from browser to host) func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) if err != nil { diff --git a/handler/overlord.go b/handler/overlord.go index 4d2575fe..a6f0df8b 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -21,7 +21,7 @@ func NewOverlordClient(oc *websocket.Conn) *OverlordClient { return oclient } -func (h *Handler) Register(oclient *OverlordClient) { +func (h *Handler) RegisterOverlordClient(oclient *OverlordClient) { // Received from overlord the serverID oclient.Receive( diff --git a/overlord/overlord.go b/overlord/overlord.go index 3e67de4a..b1e459a2 100644 --- a/overlord/overlord.go +++ b/overlord/overlord.go @@ -8,16 +8,26 @@ import ( "strconv" "github.com/giongto35/cloud-game/cws" - "github.com/giongto35/cloud-game/webrtc" + "github.com/gorilla/websocket" ) -var roomToServer = map[string]string{} +type Server struct { + roomToServer map[string]string + // servers are the map serverID to server Client + servers map[string]*cws.Client +} -// servers are the map serverID to server Client -var servers = map[string]*cws.Client{} +var upgrader = websocket.Upgrader{} + +func NewServer() *Server { + return &Server{ + servers: map[string]*cws.Client{}, + roomToServer: map[string]string{}, + } +} // If it's overlord, handle overlord connection (from host to overlord) -func WSO(w http.ResponseWriter, r *http.Request) { +func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { fmt.Println("Connected") c, err := upgrader.Upgrade(w, r, nil) if err != nil { @@ -26,51 +36,58 @@ func WSO(w http.ResponseWriter, r *http.Request) { } defer c.Close() - // register new server + // Register new server serverID := strconv.Itoa(rand.Int()) log.Println("A new server connected ", serverID) - client := NewClient(c) - servers[serverID] = client + // Register to servers map the client connection + client := cws.NewClient(c) + o.servers[serverID] = client - wssession := &Session{ - client: client, - peerconnection: webrtc.NewWebRTC(), - // The server session is maintaining - } + //wssession := &Session{ + //client: client, + //peerconnection: webrtc.NewWebRTC(), + //// The server session is maintaining + //} - client.send( - WSPacket{ + // Sendback the ID to server + client.Send( + cws.WSPacket{ ID: "serverID", Data: serverID, }, nil, ) - client.receive("registerRoom", func(resp WSPacket) WSPacket { + // registerRoom event from a server, when server created a new room. + // RoomID is global so it is managed by overlord. + client.Receive("registerRoom", func(resp cws.WSPacket) cws.WSPacket { log.Println("Received registerRoom ", resp.Data, serverID) - roomToServer[resp.Data] = serverID - return WSPacket{ + o.roomToServer[resp.Data] = serverID + return cws.WSPacket{ ID: "registerRoom", } }) - client.receive("getRoom", func(resp WSPacket) WSPacket { + // getRoom returns the server ID based on requested roomID. + client.Receive("getRoom", func(resp cws.WSPacket) cws.WSPacket { log.Println("Received a getroom request") - return WSPacket{ + return cws.WSPacket{ ID: "getRoom", - Data: roomToServer[resp.Data], + Data: o.roomToServer[resp.Data], } }) - client.receive("initwebrtc", func(resp WSPacket) WSPacket { + // Relay message from server to other target server + // TODO: Generalize + client.Receive("initwebrtc", func(resp cws.WSPacket) cws.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( + sdp := o.servers[resp.TargetHostID].SyncSend( resp, ) @@ -79,46 +96,48 @@ func WSO(w http.ResponseWriter, r *http.Request) { 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) - } + //localSession, err := wssession.peerconnection.StartClient(resp.Data, width, height) + //if err != nil { + //log.Fatalln(err) + //} - return WSPacket{ - ID: "sdp", - Data: localSession, - } + //return cws.WSPacket{ + //ID: "sdp", + //Data: localSession, + //} + return cws.EmptyPacket }) // TODO: use relay ID type // TODO: Merge sdp and start - client.receive("start", func(resp WSPacket) WSPacket { + client.Receive("start", func(resp cws.WSPacket) cws.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 := o.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") - } + //// 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, - } + //return cws.WSPacket{ + //ID: "start", + //RoomID: roomID, + //} + return cws.EmptyPacket }) - client.listeồi + client.Listen() } From 142b9d7bb49694dc84b155ca19f5749b51dad76a Mon Sep 17 00:00:00 2001 From: giongto35 Date: Wed, 1 May 2019 15:36:21 +0800 Subject: [PATCH 06/23] Refactor WIP --- cmd/main.go | 2 ++ cmd/main_test.go | 13 +++----- handler/browser.go | 78 ++++++++++++++++++++------------------------- handler/handlers.go | 28 ++++++++++++++-- handler/overlord.go | 19 ++++++----- handler/session.go | 7 +++- 6 files changed, 83 insertions(+), 64 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index ad070a5d..b57e14dc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -82,11 +82,13 @@ func main() { log.Println("Running as overlord ") initilizeOverlord() IsOverlord = true + initilizeOverlord() } 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 + initializeServer() } } diff --git a/cmd/main_test.go b/cmd/main_test.go index 45e16b07..7072433b 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -25,17 +25,13 @@ func initOverlord() *httptest.Server { return overlord } -func initServer(t *testing.T, oclient *handler.OverlordClient) *httptest.Server { - conn := connectTestOverlordServer() - handler, err := handler.NewHandler(oclient) - if err != nil { - t.Fatalf("%v", err) - } +func initServer(t *testing.T, oconn *websocket.Conn) *httptest.Server { + handler := handler.NewHandler(oconn) server := httptest.NewServer(http.HandlerFunc(handler.WS)) return server } -func connectTestOverlordServer(t *testing.T, overlordURL string) *handler.OverlordClient { +func connectTestOverlordServer(t *testing.T, overlordURL string) *websocket.Conn { if overlordURL == "" { return nil } else { @@ -47,7 +43,6 @@ func connectTestOverlordServer(t *testing.T, overlordURL string) *handler.Overlo if err != nil { t.Fatalf("%v", err) } - defer oconn.Close() return oconn } @@ -142,7 +137,7 @@ func TestSingleServerOneOverlord(t *testing.T) { o := initOverlord() defer o.Close() - oconn := connectOverlord(t, o.URL) + oconn := connectTestOverlordServer(t, o.URL) // Init slave server s := initServer(t, oconn) defer s.Close() diff --git a/handler/browser.go b/handler/browser.go index 082c22a5..e9b2bc04 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -6,20 +6,17 @@ import ( "github.com/giongto35/cloud-game/config" "github.com/giongto35/cloud-game/cws" - "github.com/giongto35/cloud-game/handler/gamelist" - "github.com/giongto35/cloud-game/webrtc" "github.com/gorilla/websocket" pionRTC "github.com/pion/webrtc" - uuid "github.com/satori/go.uuid" ) type BrowserClient struct { *cws.Client - session *Session - oclient *OverlordClient - gameName string - roomID string - playerIndex int + //session *Session + //oclient *OverlordClient + //gameName string + //roomID string + //playerIndex int } func (s *Session) RegisterBrowserClient() { @@ -39,7 +36,7 @@ func (s *Session) RegisterBrowserClient() { return cws.WSPacket{ ID: "sdp", Data: localSession, - SessionID: s.SessionID, + SessionID: s.ID, } }) @@ -80,35 +77,34 @@ func (s *Session) RegisterBrowserClient() { //}) browserClient.Receive("start", func(resp cws.WSPacket) (req cws.WSPacket) { - gameName = resp.Data - roomID = resp.RoomID - playerIndex = resp.PlayerIndex + s.GameName = resp.Data + s.RoomID = resp.RoomID + s.PlayerIndex = resp.PlayerIndex isNewRoom := false log.Println("Starting game") // If we are connecting to overlord, request serverID from roomID - if browserClient.oclient != nil { - session := browserClient.session - roomServerID := getServerIDOfRoom(session.OverlordClient, roomID) - log.Println("Server of RoomID ", roomID, " is ", roomServerID) - if roomServerID != "" && wssession.ServerID != roomServerID { + if s.OverlordClient != nil { + roomServerID := getServerIDOfRoom(s.OverlordClient, s.RoomID) + log.Println("Server of RoomID ", s.RoomID, " is ", s.RoomID) + if roomServerID != "" && s.ServerID != roomServerID { // TODO: Re -register - go bridgeConnection(wssession, roomServerID, gameName, roomID, playerIndex) + go s.bridgeConnection(roomServerID, s.GameName, s.RoomID, s.PlayerIndex) return } } - roomID, isNewRoom = startSession(wssession.peerconnection, gameName, roomID, playerIndex) + s.RoomID, isNewRoom = startSession(s.peerconnection, s.GameName, s.RoomID, s.PlayerIndex) // Register room to overlord if we are connecting to overlord - if isNewRoom && browserClient.session.OverlordClient != nil { - browserClient.session.OverlordClient.Send(cws.WSPacket{ + if isNewRoom && s.OverlordClient != nil { + s.OverlordClient.Send(cws.WSPacket{ ID: "registerRoom", - Data: roomID, + Data: s.RoomID, }, nil) } req.ID = "start" - req.RoomID = roomID - req.SessionID = sessionID + req.RoomID = s.RoomID + req.SessionID = s.ID return req }) @@ -130,31 +126,27 @@ func (s *Session) RegisterBrowserClient() { } // NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server -func NewBrowserClient(c *websocket.Conn, overlordClient *OverlordClient) *BrowserClient { - roomID := "" - gameName := "" - playerIndex := 0 +func NewBrowserClient(c *websocket.Conn) *BrowserClient { + //roomID := "" + //gameName := "" + //playerIndex := 0 // Create connection to overlord browserClient := &BrowserClient{ - Client: cws.NewClient(c), - gameName: "", - roomID: "", - playerIndex: 0, + Client: cws.NewClient(c), + //gameName: "", + //roomID: "", + //playerIndex: 0, } //sessionID := strconv.Itoa(rand.Int()) - sessionID := uuid.Must(uuid.NewV4()).String() + //sessionID := uuid.Must(uuid.NewV4()).String() - wssession := &Session{ - BrowserClient: browserClient, - OverlordClient: overlordClient, - peerconnection: webrtc.NewWebRTC(), - // The server session is maintaining - } + //wssession := &Session{ + //BrowserClient: browserClient, + //OverlordClient: overlordClient, + //peerconnection: webrtc.NewWebRTC(), + //// The server session is maintaining + //} - browserClient.Send(cws.WSPacket{ - ID: "gamelist", - Data: gamelist.GetEncodedGameList(), - }, nil) return browserClient } diff --git a/handler/handlers.go b/handler/handlers.go index b9f4ab8c..3fe49e22 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -1,6 +1,7 @@ package handler import ( + "fmt" "io/ioutil" "log" "net/http" @@ -9,6 +10,7 @@ import ( "github.com/giongto35/cloud-game/config" "github.com/giongto35/cloud-game/webrtc" "github.com/gorilla/websocket" + uuid "github.com/satori/go.uuid" ) const ( @@ -18,7 +20,7 @@ const ( debugIndex = "./static/index_ws.html" ) -var indexFN = gameboyIndex +var indexFN = debugIndex // Time allowed to write a message to the peer. var readWait = 30 * time.Second @@ -71,6 +73,27 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { log.Print("[!] WS upgrade:", err) return } + + client := NewBrowserClient(c) + //client := NewClient(c) + ////sessionID := strconv.Itoa(rand.Int()) + sessionID := uuid.Must(uuid.NewV4()).String() + wssession := &Session{ + ID: sessionID, + BrowserClient: client, + OverlordClient: h.oClient, + peerconnection: webrtc.NewWebRTC(), + } + wssession.RegisterBrowserClient() + fmt.Println("oclient : ", h.oClient) + + if wssession.OverlordClient != nil { + wssession.RegisterOverlordClient() + go wssession.OverlordClient.Heartbeat() + go wssession.OverlordClient.Listen() + } + wssession.BrowserClient.Listen() + defer c.Close() //var gameName string //var roomID string @@ -191,8 +214,7 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { //return req //}) - client := NewBrowserClient(c, oclient) - client.Listen() + //client.Listen() } func createOverlordConnection() (*websocket.Conn, error) { diff --git a/handler/overlord.go b/handler/overlord.go index a6f0df8b..d84411cf 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -11,17 +11,23 @@ import ( type OverlordClient struct { *cws.Client + peerconnections map[string]*webrtc.WebRTC } // NewOverlordClient returns a client connecting to overlord for coordiation between different server func NewOverlordClient(oc *websocket.Conn) *OverlordClient { + if oc == nil { + return nil + } + oclient := &OverlordClient{ Client: cws.NewClient(oc), } return oclient } -func (h *Handler) RegisterOverlordClient(oclient *OverlordClient) { +func (s *Session) RegisterOverlordClient() { + oclient := s.OverlordClient // Received from overlord the serverID oclient.Receive( @@ -29,7 +35,7 @@ func (h *Handler) RegisterOverlordClient(oclient *OverlordClient) { func(response cws.WSPacket) (request cws.WSPacket) { // Stick session with serverID got from overlord log.Println("Received serverID ", response.Data) - h.serverID = response.Data + s.ServerID = response.Data return cws.EmptyPacket }, @@ -45,7 +51,7 @@ func (h *Handler) RegisterOverlordClient(oclient *OverlordClient) { peerconnection := webrtc.NewWebRTC() // init new peerconnection from sessionID localSession, err := peerconnection.StartClient(resp.Data, config.Width, config.Height) - h.peerconnections[resp.SessionID] = peerconnection + oclient.peerconnections[resp.SessionID] = peerconnection if err != nil { log.Fatalln(err) @@ -66,7 +72,7 @@ func (h *Handler) RegisterOverlordClient(oclient *OverlordClient) { log.Println("Received a start request from overlord") log.Println("Add the connection to current room on the host") - peerconnection := h.peerconnections[resp.SessionID] + peerconnection := oclient.peerconnections[resp.SessionID] log.Println("start session") roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) log.Println("Done, sending back") @@ -82,9 +88,6 @@ func (h *Handler) RegisterOverlordClient(oclient *OverlordClient) { }, ) // 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() - } func getServerIDOfRoom(oc *OverlordClient, roomID string) string { @@ -100,7 +103,7 @@ func getServerIDOfRoom(oc *OverlordClient, roomID string) string { return packet.Data } -func bridgeConnection(session *Session, serverID string, gameName string, roomID string, playerIndex int) { +func (session *Session) bridgeConnection(serverID string, gameName string, roomID string, playerIndex int) { log.Println("Bridging connection to other Host ", serverID) client := session.BrowserClient // Ask client to init diff --git a/handler/session.go b/handler/session.go index 5bd13729..cb441b32 100644 --- a/handler/session.go +++ b/handler/session.go @@ -8,10 +8,15 @@ import ( // It involves one connection to browser and one connection to the overlord // Peerconnection can be from other server to ensure better latency type Session struct { + ID string BrowserClient *BrowserClient OverlordClient *OverlordClient peerconnection *webrtc.WebRTC - ServerID string + + ServerID string + GameName string + RoomID string + PlayerIndex int } // startSession handles one session call From e7567c3fa3d02db40a6e735abd1210477461d79b Mon Sep 17 00:00:00 2001 From: giongto35 Date: Wed, 1 May 2019 15:56:31 +0800 Subject: [PATCH 07/23] Refactor mode select --- cmd/main.go | 15 ++------------- cmd/main_test.go | 2 +- handler/handlers.go | 25 +++++++++++++++++++++---- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index b57e14dc..63a8deef 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -19,13 +19,10 @@ const ( 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{} // initilizeOverlord setup an overlord server @@ -56,7 +53,7 @@ func initializeServer() { log.Println("Run as a single server") } - handler := handler.NewHandler(conn) + handler := handler.NewHandler(conn, *config.IsDebug) // ignore origin upgrader.CheckOrigin = func(r *http.Request) bool { return true } @@ -71,24 +68,16 @@ func initializeServer() { func main() { flag.Parse() - log.Println("Usage: ./game [debug]") - if *config.IsDebug { - // debug - indexFN = debugIndex - log.Println("Use debug version") - } + log.Println("Usage: ./game [-debug]") if *config.OverlordHost == "overlord" { log.Println("Running as overlord ") initilizeOverlord() - IsOverlord = true - initilizeOverlord() } 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 initializeServer() } } diff --git a/cmd/main_test.go b/cmd/main_test.go index 7072433b..7d30f0ac 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -26,7 +26,7 @@ func initOverlord() *httptest.Server { } func initServer(t *testing.T, oconn *websocket.Conn) *httptest.Server { - handler := handler.NewHandler(oconn) + handler := handler.NewHandler(oconn, true) server := httptest.NewServer(http.HandlerFunc(handler.WS)) return server } diff --git a/handler/handlers.go b/handler/handlers.go index 3fe49e22..79e62337 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -8,6 +8,8 @@ import ( "time" "github.com/giongto35/cloud-game/config" + "github.com/giongto35/cloud-game/cws" + "github.com/giongto35/cloud-game/handler/gamelist" "github.com/giongto35/cloud-game/webrtc" "github.com/gorilla/websocket" uuid "github.com/satori/go.uuid" @@ -20,14 +22,11 @@ const ( debugIndex = "./static/index_ws.html" ) -var indexFN = debugIndex - // Time allowed to write a message to the peer. var readWait = 30 * time.Second var writeWait = 30 * time.Second // Flag to determine if the server is overlord or not -var IsOverlord = false var upgrader = websocket.Upgrader{} // ID to peerconnection @@ -38,6 +37,10 @@ type Handler struct { oClient *OverlordClient rooms map[string]*Room serverID string + // isDebug determines the mode handler is running + isDebug bool + isOverlord bool + // ID to peerconnection peerconnections map[string]*webrtc.WebRTC // Session @@ -45,12 +48,13 @@ type Handler struct { } // NewHandler returns a new server -func NewHandler(overlordConn *websocket.Conn) *Handler { +func NewHandler(overlordConn *websocket.Conn, isDebug bool) *Handler { //conn, err := createOverlordConnection() //if err != nil { //return nil, err //} return &Handler{ + isDebug: isDebug, oClient: NewOverlordClient(overlordConn), rooms: map[string]*Room{}, peerconnections: map[string]*webrtc.WebRTC{}, @@ -59,6 +63,13 @@ func NewHandler(overlordConn *websocket.Conn) *Handler { // GetWeb returns web frontend func (h *Handler) GetWeb(w http.ResponseWriter, r *http.Request) { + indexFN := "" + if h.isDebug { + indexFN = debugIndex + } else { + indexFN = gameboyIndex + } + bs, err := ioutil.ReadFile(indexFN) if err != nil { log.Fatal(err) @@ -92,6 +103,12 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { go wssession.OverlordClient.Heartbeat() go wssession.OverlordClient.Listen() } + + wssession.BrowserClient.Send(cws.WSPacket{ + ID: "gamelist", + Data: gamelist.GetEncodedGameList(), + }, nil) + wssession.BrowserClient.Listen() defer c.Close() From d7116f11cd3c5d7944fada563ce419fbe79b19df Mon Sep 17 00:00:00 2001 From: giongto35 Date: Wed, 1 May 2019 16:37:15 +0800 Subject: [PATCH 08/23] Update test --- cmd/main.go | 3 ++- cmd/main_test.go | 8 +++++++- handler/gamelist/games.go | 15 ++++++++------- handler/handlers.go | 9 ++++++--- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 63a8deef..b37edf59 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -17,6 +17,7 @@ import ( const ( gameboyIndex = "./static/gameboy.html" debugIndex = "./static/index_ws.html" + gamePath = "games" ) // Time allowed to write a message to the peer. @@ -53,7 +54,7 @@ func initializeServer() { log.Println("Run as a single server") } - handler := handler.NewHandler(conn, *config.IsDebug) + handler := handler.NewHandler(conn, *config.IsDebug, gamePath) // ignore origin upgrader.CheckOrigin = func(r *http.Request) bool { return true } diff --git a/cmd/main_test.go b/cmd/main_test.go index 7d30f0ac..3ff088a2 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -17,6 +17,9 @@ import ( ) var host = "http://localhost:8000" + +// Test is in cmd, so gamePath is in parent path +var testGamePath = "../games" var webrtcconfig = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}} func initOverlord() *httptest.Server { @@ -26,7 +29,8 @@ func initOverlord() *httptest.Server { } func initServer(t *testing.T, oconn *websocket.Conn) *httptest.Server { - handler := handler.NewHandler(oconn, true) + fmt.Println("Spawn new server") + handler := handler.NewHandler(oconn, true, testGamePath) server := httptest.NewServer(http.HandlerFunc(handler.WS)) return server } @@ -52,6 +56,7 @@ func initClient(t *testing.T, host string) { u := "ws" + strings.TrimPrefix(host, "http") // Connect to the server + fmt.Println("Connecting to server") ws, _, err := websocket.DefaultDialer.Dial(u, nil) if err != nil { t.Fatalf("%v", err) @@ -60,6 +65,7 @@ func initClient(t *testing.T, host string) { // Simulate peerconnection initialization from client + fmt.Println("Simulating PeerConnection") peerConnection, err := webrtc.NewPeerConnection(webrtcconfig) if err != nil { t.Fatalf("%v", err) diff --git a/handler/gamelist/games.go b/handler/gamelist/games.go index 6d0cdf4a..8cc714b8 100644 --- a/handler/gamelist/games.go +++ b/handler/gamelist/games.go @@ -2,18 +2,18 @@ package gamelist import ( "encoding/json" + "fmt" "os" "path/filepath" ) -const gamePath = "games" - // getGameList returns list of games stored in games -func GetGameList() []string { +// TODO: change to class +func GetGameList(gamePath string) []string { var games []string filepath.Walk(gamePath, func(path string, info os.FileInfo, err error) error { - if !info.IsDir() { - // remove prefix + if info != nil && !info.IsDir() { + // Remove prefix to obtain file names path = path[len(gamePath)+1:] // Add to games list games = append(games, path) @@ -25,7 +25,8 @@ func GetGameList() []string { } // GetEncodedGameList returns game list in encoded wspacket format -func GetEncodedGameList() string { - encodedList, _ := json.Marshal(GetGameList()) +func GetEncodedGameList(gamePath string) string { + encodedList, _ := json.Marshal(GetGameList(gamePath)) + fmt.Println(encodedList) return string(encodedList) } diff --git a/handler/handlers.go b/handler/handlers.go index 79e62337..918b6b47 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -40,6 +40,7 @@ type Handler struct { // isDebug determines the mode handler is running isDebug bool isOverlord bool + gamePath string // ID to peerconnection peerconnections map[string]*webrtc.WebRTC @@ -48,16 +49,18 @@ type Handler struct { } // NewHandler returns a new server -func NewHandler(overlordConn *websocket.Conn, isDebug bool) *Handler { +func NewHandler(overlordConn *websocket.Conn, isDebug bool, gamePath string) *Handler { //conn, err := createOverlordConnection() //if err != nil { //return nil, err //} return &Handler{ - isDebug: isDebug, oClient: NewOverlordClient(overlordConn), rooms: map[string]*Room{}, peerconnections: map[string]*webrtc.WebRTC{}, + + isDebug: isDebug, + gamePath: gamePath, } } @@ -106,7 +109,7 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { wssession.BrowserClient.Send(cws.WSPacket{ ID: "gamelist", - Data: gamelist.GetEncodedGameList(), + Data: gamelist.GetEncodedGameList(h.gamePath), }, nil) wssession.BrowserClient.Listen() From 8d0dfb1943f57374a6105920d003d6f3fe7adf26 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Wed, 1 May 2019 21:06:05 +0800 Subject: [PATCH 09/23] Separate create room logic --- cmd/main_test.go | 1 - handler/browser.go | 9 ++++--- handler/handlers.go | 1 + handler/overlord.go | 19 +++++++++++--- handler/rooms.go | 59 ++++++++++++++++++++++++-------------------- handler/session.go | 60 ++++++++++++++++++++++++++++++++++++--------- 6 files changed, 103 insertions(+), 46 deletions(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index 3ff088a2..1ae03a1f 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -116,7 +116,6 @@ func initClient(t *testing.T, host string) { RoomID: "", PlayerIndex: 1, }, func(resp cws.WSPacket) { - fmt.Println("Received response") fmt.Println("RoomID:", resp.RoomID) roomID <- resp.RoomID }) diff --git a/handler/browser.go b/handler/browser.go index e9b2bc04..5f7fdcb2 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -2,6 +2,7 @@ package handler import ( "encoding/json" + "fmt" "log" "github.com/giongto35/cloud-game/config" @@ -80,7 +81,6 @@ func (s *Session) RegisterBrowserClient() { s.GameName = resp.Data s.RoomID = resp.RoomID s.PlayerIndex = resp.PlayerIndex - isNewRoom := false log.Println("Starting game") // If we are connecting to overlord, request serverID from roomID @@ -94,9 +94,11 @@ func (s *Session) RegisterBrowserClient() { } } - s.RoomID, isNewRoom = startSession(s.peerconnection, s.GameName, s.RoomID, s.PlayerIndex) + room := s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) + room.addConnectionToRoom(s.peerconnection, s.PlayerIndex) + s.RoomID = room.ID // Register room to overlord if we are connecting to overlord - if isNewRoom && s.OverlordClient != nil { + if room != nil && s.OverlordClient != nil { s.OverlordClient.Send(cws.WSPacket{ ID: "registerRoom", Data: s.RoomID, @@ -105,6 +107,7 @@ func (s *Session) RegisterBrowserClient() { req.ID = "start" req.RoomID = s.RoomID req.SessionID = s.ID + fmt.Println("Response from start", req) return req }) diff --git a/handler/handlers.go b/handler/handlers.go index 918b6b47..60bbde51 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -97,6 +97,7 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { BrowserClient: client, OverlordClient: h.oClient, peerconnection: webrtc.NewWebRTC(), + handler: h, } wssession.RegisterBrowserClient() fmt.Println("oclient : ", h.oClient) diff --git a/handler/overlord.go b/handler/overlord.go index d84411cf..62f5e4b0 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -9,6 +9,8 @@ import ( "github.com/gorilla/websocket" ) +// OverlordClient maintans connection to overlord +// We expect only one OverlordClient for each server type OverlordClient struct { *cws.Client peerconnections map[string]*webrtc.WebRTC @@ -26,6 +28,7 @@ func NewOverlordClient(oc *websocket.Conn) *OverlordClient { return oclient } +// RegisterOverlordClient routes overlord Client func (s *Session) RegisterOverlordClient() { oclient := s.OverlordClient @@ -64,7 +67,7 @@ func (s *Session) RegisterOverlordClient() { }, ) - // Received start from overlord. This is happens when bridging + // Received start from overlord. This happens when bridging // TODO: refactor oclient.Receive( "start", @@ -74,16 +77,24 @@ func (s *Session) RegisterOverlordClient() { peerconnection := oclient.peerconnections[resp.SessionID] log.Println("start session") - roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) + + //room := s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) + room := s.handler.getRoom(s.RoomID) + if room == nil { + log.Println("Room not found", s.RoomID) + return cws.EmptyPacket + } + room.addConnectionToRoom(peerconnection, s.PlayerIndex) + //roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) log.Println("Done, sending back") // Bridge always access to old room // TODO: log warn - if isNewRoom == true { + if room != nil { log.Fatal("Bridge should not spawn new room") } req.ID = "start" - req.RoomID = roomID + req.RoomID = room.ID return req }, ) diff --git a/handler/rooms.go b/handler/rooms.go index 393da503..dbd41732 100644 --- a/handler/rooms.go +++ b/handler/rooms.go @@ -14,6 +14,8 @@ import ( // 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 { + ID string + imageChannel chan *image.RGBA audioChannel chan float32 inputChannel chan int @@ -36,7 +38,7 @@ func generateRoomID() string { } // init initilizes a room returns roomID -func initRoom(roomID, gameName string) string { +func (h *Handler) initRoom(roomID, gameName string) *Room { // if no roomID is given, generate it if roomID == "" { roomID = generateRoomID() @@ -50,6 +52,8 @@ func initRoom(roomID, gameName string) string { director := emulator.NewDirector(roomID, imageChannel, audioChannel, inputChannel) room := &Room{ + ID: roomID, + imageChannel: imageChannel, audioChannel: audioChannel, inputChannel: inputChannel, @@ -58,18 +62,17 @@ func initRoom(roomID, gameName string) string { director: director, Done: make(chan struct{}), } - rooms[roomID] = room go room.startVideo() go room.startAudio() - go director.Start([]string{"games/" + gameName}) + go director.Start([]string{"../games/" + gameName}) - return roomID + return room } // isRoomRunning check if there is any running sessions. // TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. -func isRoomRunning(roomID string) bool { +func (h *Handler) isRoomRunning(roomID string) bool { // If no roomID is registered if _, ok := rooms[roomID]; !ok { return false @@ -84,24 +87,32 @@ func isRoomRunning(roomID string) bool { return false } +func (r *Room) addConnectionToRoom(peerconnection *webrtc.WebRTC, playerIndex int) { + r.cleanSession(peerconnection) + peerconnection.AttachRoomID(r.ID) + go r.startWebRTCSession(peerconnection, playerIndex) + + r.rtcSessions = append(r.rtcSessions, peerconnection) +} + // 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) +func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC, playerIndex int) { + inputChannel := r.inputChannel + log.Println("room, inputChannel", r, inputChannel) for { select { - case <-webRTC.Done: - removeSession(webRTC, room) + case <-peerconnection.Done: + r.removeSession(peerconnection) default: } // Client stopped - if webRTC.IsClosed() { + if peerconnection.IsClosed() { return } // encode frame - if webRTC.IsConnected() { - input := <-webRTC.InputChannel + if peerconnection.IsConnected() { + input := <-peerconnection.InputChannel // the first 8 bits belong to player 1 // the next 8 belongs to player 2 ... // We standardize and put it to inputChannel (16 bits) @@ -111,26 +122,22 @@ func startWebRTCSession(room *Room, webRTC *webrtc.WebRTC, playerIndex int) { } } -func cleanSession(w *webrtc.WebRTC) { - room, ok := rooms[w.RoomID] - if !ok { - return - } - removeSession(w, room) +func (r *Room) cleanSession(peerconnection *webrtc.WebRTC) { + r.removeSession(peerconnection) } -func removeSession(w *webrtc.WebRTC, room *Room) { - room.sessionsLock.Lock() - defer room.sessionsLock.Unlock() - for i, s := range room.rtcSessions { +func (r *Room) removeSession(w *webrtc.WebRTC) { + r.sessionsLock.Lock() + defer r.sessionsLock.Unlock() + for i, s := range r.rtcSessions { if s == w { - room.rtcSessions = append(room.rtcSessions[:i], room.rtcSessions[i+1:]...) + r.rtcSessions = append(r.rtcSessions[:i], r.rtcSessions[i+1:]...) break } } // If room has no sessions, close room - if len(room.rtcSessions) == 0 { - room.Done <- struct{}{} + if len(r.rtcSessions) == 0 { + r.Done <- struct{}{} } } diff --git a/handler/session.go b/handler/session.go index cb441b32..6cfd80a7 100644 --- a/handler/session.go +++ b/handler/session.go @@ -13,30 +13,66 @@ type Session struct { OverlordClient *OverlordClient peerconnection *webrtc.WebRTC + // TODO: Decouple this + handler *Handler + ServerID string GameName string RoomID string PlayerIndex int } -// startSession handles one session call -func startSession(webRTC *webrtc.WebRTC, gameName string, roomID string, playerIndex int) (rRoomID string, isNewRoom bool) { - isNewRoom = false - cleanSession(webRTC) +// getRoom returns room from roomID +func (h *Handler) getRoom(roomID string) *Room { + room, ok := h.rooms[roomID] + if !ok { + return nil + } + + return room +} + +// createNewRoom creates a new room +// Return nil in case of room is existed +func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) *Room { + //cleanSession(peerconnection) // 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 + if roomID == "" || !h.isRoomRunning(roomID) { + room := h.initRoom(roomID, gameName) + h.rooms[roomID] = room + return room } // TODO: Might have race condition - rooms[roomID].rtcSessions = append(rooms[roomID].rtcSessions, webRTC) - room := rooms[roomID] + //rooms[roomID].rtcSessions = append(rooms[roomID].rtcSessions, peerconnection) + //room := rooms[roomID] - webRTC.AttachRoomID(roomID) - go startWebRTCSession(room, webRTC, playerIndex) + //peerconnection.AttachRoomID(roomID) + //go startWebRTCSession(room, peerconnection, playerIndex) - return roomID, isNewRoom + return nil } + +// startPeerConnection handles one peerconnection call +//func (s *Handler) startPeerConnection( gameName string, roomID string, playerIndex int) (rRoomID string, isNewRoom bool) { +//isNewRoom = false +//cleanSession(peerconnection) +//// 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, peerconnection) +//room := rooms[roomID] + +//peerconnection.AttachRoomID(roomID) +//go startWebRTCSession(room, peerconnection, playerIndex) + +//return roomID, isNewRoom +//} From 041e8059631f148f01e956431d12b5d2d1ff2346 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Wed, 1 May 2019 21:10:24 +0800 Subject: [PATCH 10/23] Add rooms to handler --- handler/rooms.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/handler/rooms.go b/handler/rooms.go index dbd41732..f6ce1941 100644 --- a/handler/rooms.go +++ b/handler/rooms.go @@ -28,8 +28,6 @@ type Room struct { director *emulator.Director } -var rooms = map[string]*Room{} - // generateRoomID generate a unique room ID containing 16 digits func generateRoomID() string { roomID := strconv.FormatInt(rand.Int63(), 16) @@ -74,12 +72,12 @@ func (h *Handler) initRoom(roomID, gameName string) *Room { // TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. func (h *Handler) isRoomRunning(roomID string) bool { // If no roomID is registered - if _, ok := rooms[roomID]; !ok { + if _, ok := h.rooms[roomID]; !ok { return false } // If there is running session - for _, s := range rooms[roomID].rtcSessions { + for _, s := range h.rooms[roomID].rtcSessions { if !s.IsClosed() { return true } From 5320283f6362e9996c5ecb4e408e9ea91d824322 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Wed, 1 May 2019 21:36:01 +0800 Subject: [PATCH 11/23] Clean code --- cmd/main.go | 4 -- handler/browser.go | 26 +------ handler/handlers.go | 162 +++++++------------------------------------- handler/overlord.go | 14 ++-- handler/rooms.go | 16 ++--- handler/session.go | 55 --------------- 6 files changed, 38 insertions(+), 239 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index b37edf59..57ab7fca 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,7 +6,6 @@ import ( "net/http" _ "net/http/pprof" "strings" - "time" "github.com/giongto35/cloud-game/config" "github.com/giongto35/cloud-game/handler" @@ -21,9 +20,6 @@ const ( ) // Time allowed to write a message to the peer. -var readWait = 30 * time.Second -var writeWait = 30 * time.Second - var upgrader = websocket.Upgrader{} // initilizeOverlord setup an overlord server diff --git a/handler/browser.go b/handler/browser.go index 5f7fdcb2..4d4fbde3 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -13,11 +13,6 @@ import ( type BrowserClient struct { *cws.Client - //session *Session - //oclient *OverlordClient - //gameName string - //roomID string - //playerIndex int } func (s *Session) RegisterBrowserClient() { @@ -130,26 +125,7 @@ func (s *Session) RegisterBrowserClient() { // NewOverlordClient returns a client connecting to browser. This connection exchanges information between clients and server func NewBrowserClient(c *websocket.Conn) *BrowserClient { - //roomID := "" - //gameName := "" - //playerIndex := 0 - // Create connection to overlord - browserClient := &BrowserClient{ + return &BrowserClient{ Client: cws.NewClient(c), - //gameName: "", - //roomID: "", - //playerIndex: 0, } - - //sessionID := strconv.Itoa(rand.Int()) - //sessionID := uuid.Must(uuid.NewV4()).String() - - //wssession := &Session{ - //BrowserClient: browserClient, - //OverlordClient: overlordClient, - //peerconnection: webrtc.NewWebRTC(), - //// The server session is maintaining - //} - - return browserClient } diff --git a/handler/handlers.go b/handler/handlers.go index 60bbde51..7e0de6cf 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -7,7 +7,6 @@ import ( "net/http" "time" - "github.com/giongto35/cloud-game/config" "github.com/giongto35/cloud-game/cws" "github.com/giongto35/cloud-game/handler/gamelist" "github.com/giongto35/cloud-game/webrtc" @@ -16,8 +15,6 @@ import ( ) const ( - scale = 3 - title = "NES" gameboyIndex = "./static/gameboy.html" debugIndex = "./static/index_ws.html" ) @@ -29,10 +26,6 @@ var writeWait = 30 * time.Second // Flag to determine if the server is overlord or not var upgrader = websocket.Upgrader{} -// ID to peerconnection -//var peerconnections = map[string]*webrtc.WebRTC{} -//var oclient *OverlordClient - type Handler struct { oClient *OverlordClient rooms map[string]*Room @@ -50,10 +43,6 @@ type Handler struct { // NewHandler returns a new server func NewHandler(overlordConn *websocket.Conn, isDebug bool, gamePath string) *Handler { - //conn, err := createOverlordConnection() - //if err != nil { - //return nil, err - //} return &Handler{ oClient: NewOverlordClient(overlordConn), rooms: map[string]*Room{}, @@ -83,6 +72,8 @@ func (h *Handler) GetWeb(w http.ResponseWriter, r *http.Request) { // WS handles normal traffic (from browser to host) func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { c, err := upgrader.Upgrade(w, r, nil) + defer c.Close() + if err != nil { log.Print("[!] WS upgrade:", err) return @@ -114,135 +105,30 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { }, nil) wssession.BrowserClient.Listen() - - defer c.Close() - //var gameName string - //var roomID string - //var playerIndex int - - //// 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.send(WSPacket{ - //ID: "gamelist", - //Data: getEncodedGameList(), - //}, nil) - - //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() } -func createOverlordConnection() (*websocket.Conn, error) { - c, _, err := websocket.DefaultDialer.Dial(*config.OverlordHost, nil) - if err != nil { - return nil, err +// getRoom returns room from roomID +func (h *Handler) getRoom(roomID string) *Room { + room, ok := h.rooms[roomID] + if !ok { + return nil } - return c, nil + return room +} + +// createNewRoom creates a new room +// Return nil in case of room is existed +func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) *Room { + // If the roomID is empty, + // or the roomID doesn't have any running sessions (room was closed) + // we spawn a new room + if roomID == "" || !h.isRoomRunning(roomID) { + room := NewRoom(roomID, gameName) + // TODO: Might have race condition + h.rooms[roomID] = room + return room + } + + return nil } diff --git a/handler/overlord.go b/handler/overlord.go index 62f5e4b0..c6fd13c3 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -114,9 +114,9 @@ func getServerIDOfRoom(oc *OverlordClient, roomID string) string { return packet.Data } -func (session *Session) bridgeConnection(serverID string, gameName string, roomID string, playerIndex int) { +func (s *Session) bridgeConnection(serverID string, gameName string, roomID string, playerIndex int) { log.Println("Bridging connection to other Host ", serverID) - client := session.BrowserClient + client := s.BrowserClient // Ask client to init log.Println("Requesting offer to browser", serverID) @@ -128,20 +128,16 @@ func (session *Session) bridgeConnection(serverID string, gameName string, roomI 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 := session.OverlordClient.SyncSend(resp) + remoteTargetSDP := s.OverlordClient.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, - //}) - session.BrowserClient.Send(cws.WSPacket{ + s.BrowserClient.Send(cws.WSPacket{ ID: "sdp", Data: remoteTargetSDP.Data, }, nil) log.Println("Init session done, start game on target host") - session.OverlordClient.SyncSend(cws.WSPacket{ + s.OverlordClient.SyncSend(cws.WSPacket{ ID: "start", Data: gameName, TargetHostID: serverID, diff --git a/handler/rooms.go b/handler/rooms.go index f6ce1941..1be88557 100644 --- a/handler/rooms.go +++ b/handler/rooms.go @@ -28,15 +28,8 @@ type Room struct { director *emulator.Director } -// 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 -} - // init initilizes a room returns roomID -func (h *Handler) initRoom(roomID, gameName string) *Room { +func NewRoom(roomID, gameName string) *Room { // if no roomID is given, generate it if roomID == "" { roomID = generateRoomID() @@ -68,6 +61,13 @@ func (h *Handler) initRoom(roomID, gameName string) *Room { return room } +// 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 +} + // isRoomRunning check if there is any running sessions. // TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. func (h *Handler) isRoomRunning(roomID string) bool { diff --git a/handler/session.go b/handler/session.go index 6cfd80a7..c344693d 100644 --- a/handler/session.go +++ b/handler/session.go @@ -21,58 +21,3 @@ type Session struct { RoomID string PlayerIndex int } - -// getRoom returns room from roomID -func (h *Handler) getRoom(roomID string) *Room { - room, ok := h.rooms[roomID] - if !ok { - return nil - } - - return room -} - -// createNewRoom creates a new room -// Return nil in case of room is existed -func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) *Room { - //cleanSession(peerconnection) - // If the roomID is empty, - // or the roomID doesn't have any running sessions (room was closed) - // we spawn a new room - if roomID == "" || !h.isRoomRunning(roomID) { - room := h.initRoom(roomID, gameName) - h.rooms[roomID] = room - return room - } - - // TODO: Might have race condition - //rooms[roomID].rtcSessions = append(rooms[roomID].rtcSessions, peerconnection) - //room := rooms[roomID] - - //peerconnection.AttachRoomID(roomID) - //go startWebRTCSession(room, peerconnection, playerIndex) - - return nil -} - -// startPeerConnection handles one peerconnection call -//func (s *Handler) startPeerConnection( gameName string, roomID string, playerIndex int) (rRoomID string, isNewRoom bool) { -//isNewRoom = false -//cleanSession(peerconnection) -//// 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, peerconnection) -//room := rooms[roomID] - -//peerconnection.AttachRoomID(roomID) -//go startWebRTCSession(room, peerconnection, playerIndex) - -//return roomID, isNewRoom -//} From d9febbf0a98f690b21e58323134a07cf54d00eb9 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Thu, 2 May 2019 03:01:58 +0800 Subject: [PATCH 12/23] Fix websocket --- cmd/main_test.go | 170 +++++++++++++++++++++++++++++++++++++------ cws/cws.go | 37 +++++++--- handler/browser.go | 11 ++- handler/handlers.go | 31 ++++---- handler/overlord.go | 3 +- overlord/overlord.go | 18 ++--- webrtc/webrtc.go | 15 ++-- 7 files changed, 212 insertions(+), 73 deletions(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index 1ae03a1f..6f43b944 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log" "net/http" "net/http/httptest" "strings" @@ -51,7 +52,7 @@ func connectTestOverlordServer(t *testing.T, overlordURL string) *websocket.Conn return oconn } -func initClient(t *testing.T, host string) { +func initClient(t *testing.T, host string) (conn *websocket.Conn, roomID chan string) { // Convert http://127.0.0.1 to ws://127.0.0. u := "ws" + strings.TrimPrefix(host, "http") @@ -61,10 +62,8 @@ func initClient(t *testing.T, host string) { if err != nil { t.Fatalf("%v", err) } - defer ws.Close() // Simulate peerconnection initialization from client - fmt.Println("Simulating PeerConnection") peerConnection, err := webrtc.NewPeerConnection(webrtcconfig) if err != nil { @@ -83,6 +82,7 @@ func initClient(t *testing.T, host string) { } // Send offer to server + log.Println("Browser Client") client := cws.NewClient(ws) go client.Listen() @@ -109,7 +109,7 @@ func initClient(t *testing.T, host string) { time.Sleep(time.Second * 3) fmt.Println("Sending start...") - roomID := make(chan string) + roomID = make(chan string) client.Send(cws.WSPacket{ ID: "start", Data: "Contra.nes", @@ -120,13 +120,7 @@ func initClient(t *testing.T, host string) { roomID <- resp.RoomID }) - respRoomID := <-roomID - if respRoomID == "" { - fmt.Println("RoomID should not be empty") - t.Fail() - } - fmt.Println("Done") - ws.Close() + return ws, roomID // If receive roomID, the server is running correctly } @@ -135,7 +129,16 @@ func TestSingleServerNoOverlord(t *testing.T) { s := initServer(t, nil) defer s.Close() - initClient(t, s.URL) + conn, roomID := initClient(t, s.URL) + defer conn.Close() + + respRoomID := <-roomID + if respRoomID == "" { + fmt.Println("RoomID should not be empty") + t.Fail() + } + fmt.Println("Done") + conn.Close() } func TestSingleServerOneOverlord(t *testing.T) { @@ -143,22 +146,141 @@ func TestSingleServerOneOverlord(t *testing.T) { defer o.Close() oconn := connectTestOverlordServer(t, o.URL) + defer oconn.Close() // Init slave server s := initServer(t, oconn) defer s.Close() - initClient(t, s.URL) + conn, roomID := initClient(t, s.URL) + respRoomID := <-roomID + if respRoomID == "" { + fmt.Println("RoomID should not be empty") + t.Fail() + } + fmt.Println("Done") + conn.Close() } -//func TestTwoServerOneOverlord(t *testing.T) { -//o := initOverlord() -//defer o.Close() -//// Init slave server -//s1 := initServer(t, o.URL) -//defer s1.Close() -//s2 := initServer(t, o.URL) -//defer s2.Close() +func TestTwoServerOneOverlord(t *testing.T) { + o := initOverlord() + defer o.Close() -//initClient(t, s1.URL) -//oclient.conn.Close() -//} + oconn1 := connectTestOverlordServer(t, o.URL) + // Init slave server + s1 := initServer(t, oconn1) + defer s1.Close() + + oconn2 := connectTestOverlordServer(t, o.URL) + // TODO: two different oconn + s2 := initServer(t, oconn2) + defer s2.Close() + + conn1, roomID := initClient(t, s1.URL) + respRoomID := <-roomID + if respRoomID == "" { + fmt.Println("RoomID should not be empty") + t.Fail() + } + fmt.Println("Done create a room in server 1") + + fmt.Println("Request the room from server 2", respRoomID) + conn2, roomID := initClient2(t, s2.URL, respRoomID) + respRoomID = <-roomID + if respRoomID == "" { + fmt.Println("RoomID should not be empty") + t.Fail() + } + + fmt.Println("Done") + conn1.Close() + conn2.Close() +} + +func initClient2(t *testing.T, host string, remoteRoomID string) (conn *websocket.Conn, roomID chan string) { + // Convert http://127.0.0.1 to ws://127.0.0. + u := "ws" + strings.TrimPrefix(host, "http") + + // Connect to the server + fmt.Println("Connecting to server") + ws, _, err := websocket.DefaultDialer.Dial(u, nil) + if err != nil { + t.Fatalf("%v", err) + } + //defer ws.Close() + + // Simulate peerconnection initialization from client + + fmt.Println("Simulating PeerConnection") + peerConnection, err := webrtc.NewPeerConnection(webrtcconfig) + if err != nil { + t.Fatalf("%v", err) + } + + offer, err := peerConnection.CreateOffer(nil) + if err != nil { + t.Fatalf("%v", err) + } + + // Sets the LocalDescription, and starts our UDP listeners + err = peerConnection.SetLocalDescription(offer) + if err != nil { + panic(err) + } + + // Send offer to server + log.Println("Browser Client") + client := cws.NewClient(ws) + go client.Listen() + + fmt.Println("Sending offer...") + client.Send(cws.WSPacket{ + ID: "initwebrtc", + Data: gamertc.Encode(offer), + }, nil) + fmt.Println("Waiting sdp...") + + client.Receive("sdp", func(resp cws.WSPacket) cws.WSPacket { + fmt.Println("received", resp.Data) + answer := webrtc.SessionDescription{} + gamertc.Decode(resp.Data, &answer) + // Apply the answer as the remote description + err = peerConnection.SetRemoteDescription(answer) + if err != nil { + panic(err) + } + + return cws.EmptyPacket + }) + + time.Sleep(time.Second * 3) + fmt.Println("Sending start...") + + // Doing the same create local room. + localRoomID := make(chan string) + client.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: "", + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + localRoomID <- resp.RoomID + }) + + <-localRoomID + + log.Println("Server2 trying to join server1 room") + // After trying loging in to one session, login to other with the roomID + client.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: remoteRoomID, + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + roomID <- resp.RoomID + }) + + // If receive roomID, the server is running correctly + return ws, roomID +} diff --git a/cws/cws.go b/cws/cws.go index b943ad55..f4a821de 100644 --- a/cws/cws.go +++ b/cws/cws.go @@ -11,6 +11,8 @@ import ( ) type Client struct { + id string + conn *websocket.Conn sendLock sync.Mutex @@ -37,9 +39,12 @@ type WSPacket struct { var EmptyPacket = WSPacket{} 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, @@ -56,20 +61,23 @@ func (c *Client) Send(request WSPacket, callback func(response WSPacket)) { } // TODO: Consider using lock free + // Wrap callback with sessionID and packetID + if callback != nil { + wrapperCallback := func(resp WSPacket) { + resp.PacketID = request.PacketID + resp.SessionID = request.SessionID + callback(resp) + } + c.sendCallbackLock.Lock() + c.sendCallback[request.PacketID] = wrapperCallback + c.sendCallbackLock.Unlock() + } + //log.Println("Registered requested callback", "ID :", request.ID, "PacketID: ", request.PacketID) + //log.Println("Callback waiting list:", c.id, c.sendCallback) + 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 @@ -79,6 +87,7 @@ func (c *Client) Receive(id string, f func(response WSPacket) (request WSPacket) // Add Meta data req.PacketID = response.PacketID req.SessionID = response.SessionID + //log.Println("Sending back request", req, "PacketID: ", req.PacketID, "SessionID: ", req.SessionID) // Skip rqeuest if it is EmptyPacket if req == EmptyPacket { @@ -125,6 +134,8 @@ func (c *Client) Listen() { } wspacket := WSPacket{} err = json.Unmarshal(rawMsg, &wspacket) + //log.Println( "Received: ", wspacket) + if err != nil { continue } @@ -132,16 +143,20 @@ func (c *Client) Listen() { // Check if some async send is waiting for the response based on packetID // TODO: Change to read lock c.sendCallbackLock.Lock() + //log.Println("Listening: Callback waiting list: ", c.id, c.sendCallback) callback, ok := c.sendCallback[wspacket.PacketID] + //log.Println("Has callback: ", ok, "ClientID: ", c.id, "PacketID ", wspacket.PacketID) c.sendCallbackLock.Unlock() if ok { go callback(wspacket) c.sendCallbackLock.Lock() + //log.Println("Deleteing Packet ", wspacket.PacketID) delete(c.sendCallback, wspacket.PacketID) c.sendCallbackLock.Unlock() // Skip receiveCallback to avoid duplication continue } + //log.Println("Listening: Callback waiting list: ", c.id, c.recvCallback) // Check if some receiver with the ID is registered if callback, ok := c.recvCallback[wspacket.ID]; ok { go callback(wspacket) diff --git a/handler/browser.go b/handler/browser.go index 4d4fbde3..7b7817c5 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -78,22 +78,29 @@ func (s *Session) RegisterBrowserClient() { s.PlayerIndex = resp.PlayerIndex log.Println("Starting game") - // If we are connecting to overlord, request serverID from roomID + // If we are connecting to overlord, request corresponding serverID based on roomID if s.OverlordClient != nil { roomServerID := getServerIDOfRoom(s.OverlordClient, s.RoomID) - log.Println("Server of RoomID ", s.RoomID, " is ", s.RoomID) + log.Println("Server of RoomID ", s.RoomID, " is ", roomServerID, " while current server is ", s.ServerID) + // If the target serverID is different from current serverID if roomServerID != "" && s.ServerID != roomServerID { // TODO: Re -register + // Bridge Connection to the target serverID go s.bridgeConnection(roomServerID, s.GameName, s.RoomID, s.PlayerIndex) return } } + // Create new room + // TODO: check if roomID is in the current server room := s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) + // Attach peerconnection to room room.addConnectionToRoom(s.peerconnection, s.PlayerIndex) s.RoomID = room.ID // Register room to overlord if we are connecting to overlord + log.Println("Try Registering room", room, "Client: ", s.OverlordClient) if room != nil && s.OverlordClient != nil { + log.Println("Registering room", s.RoomID) s.OverlordClient.Send(cws.WSPacket{ ID: "registerRoom", Data: s.RoomID, diff --git a/handler/handlers.go b/handler/handlers.go index 7e0de6cf..ed50effe 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -5,7 +5,6 @@ import ( "io/ioutil" "log" "net/http" - "time" "github.com/giongto35/cloud-game/cws" "github.com/giongto35/cloud-game/handler/gamelist" @@ -19,30 +18,28 @@ const ( debugIndex = "./static/index_ws.html" ) -// Time allowed to write a message to the peer. -var readWait = 30 * time.Second -var writeWait = 30 * time.Second - // Flag to determine if the server is overlord or not var upgrader = websocket.Upgrader{} type Handler struct { - oClient *OverlordClient - rooms map[string]*Room + // Client that connects to overlord + oClient *OverlordClient + // Rooms map : RoomID -> Room + rooms map[string]*Room + // ID of the current server globalwise serverID string // isDebug determines the mode handler is running - isDebug bool - isOverlord bool - gamePath string - - // ID to peerconnection + isDebug bool + // Path to game list + gamePath string + // All webrtc peerconnections are handled by the server + // ID -> peerconnections peerconnections map[string]*webrtc.WebRTC - // Session - wssession Session } // NewHandler returns a new server func NewHandler(overlordConn *websocket.Conn, isDebug bool, gamePath string) *Handler { + log.Println("new OverlordClient") return &Handler{ oClient: NewOverlordClient(overlordConn), rooms: map[string]*Room{}, @@ -90,15 +87,15 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { peerconnection: webrtc.NewWebRTC(), handler: h, } - wssession.RegisterBrowserClient() - fmt.Println("oclient : ", h.oClient) - if wssession.OverlordClient != nil { wssession.RegisterOverlordClient() go wssession.OverlordClient.Heartbeat() go wssession.OverlordClient.Listen() } + wssession.RegisterBrowserClient() + fmt.Println("oclient : ", h.oClient) + wssession.BrowserClient.Send(cws.WSPacket{ ID: "gamelist", Data: gamelist.GetEncodedGameList(h.gamePath), diff --git a/handler/overlord.go b/handler/overlord.go index c6fd13c3..08de552e 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -79,6 +79,7 @@ func (s *Session) RegisterOverlordClient() { log.Println("start session") //room := s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) + // Request room from Server if roomID is existed on the server room := s.handler.getRoom(s.RoomID) if room == nil { log.Println("Room not found", s.RoomID) @@ -109,7 +110,7 @@ func getServerIDOfRoom(oc *OverlordClient, roomID string) string { Data: roomID, }, ) - log.Println("Received roomID from overlord") + log.Println("Received roomID from overlord ", packet.Data) return packet.Data } diff --git a/overlord/overlord.go b/overlord/overlord.go index b1e459a2..c73527a7 100644 --- a/overlord/overlord.go +++ b/overlord/overlord.go @@ -31,14 +31,14 @@ func (o *Server) 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) + log.Print("Overlord: [!] WS upgrade:", err) return } defer c.Close() // Register new server serverID := strconv.Itoa(rand.Int()) - log.Println("A new server connected ", serverID) + log.Println("Overlord: A new server connected to Overlord", serverID) // Register to servers map the client connection client := cws.NewClient(c) @@ -62,7 +62,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { // registerRoom event from a server, when server created a new room. // RoomID is global so it is managed by overlord. client.Receive("registerRoom", func(resp cws.WSPacket) cws.WSPacket { - log.Println("Received registerRoom ", resp.Data, serverID) + log.Println("Overlord: Received registerRoom ", resp.Data, serverID) o.roomToServer[resp.Data] = serverID return cws.WSPacket{ ID: "registerRoom", @@ -71,7 +71,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { // getRoom returns the server ID based on requested roomID. client.Receive("getRoom", func(resp cws.WSPacket) cws.WSPacket { - log.Println("Received a getroom request") + log.Println("Overlord: Received a getroom request") return cws.WSPacket{ ID: "getRoom", Data: o.roomToServer[resp.Data], @@ -81,10 +81,10 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { // Relay message from server to other target server // TODO: Generalize client.Receive("initwebrtc", func(resp cws.WSPacket) cws.WSPacket { - log.Println("Received a relay sdp request from a host") + log.Println("Overlord: Received a relay sdp request from a host") // TODO: Abstract if resp.TargetHostID != serverID { - log.Println("sending relay sdp to target host", resp) + log.Println("Overlord: Sending relay sdp to target host", resp) // relay SDP to target host and get back sdp // TODO: Async sdp := o.servers[resp.TargetHostID].SyncSend( @@ -93,7 +93,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { return sdp } - log.Println("Target host is overlord itself: start peerconnection") + log.Println("Overlord: 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) @@ -111,7 +111,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { // TODO: use relay ID type // TODO: Merge sdp and start client.Receive("start", func(resp cws.WSPacket) cws.WSPacket { - log.Println("Received a relay start request from a host") + log.Println("Overlord: Received a relay start request from a host") // TODO: Abstract if resp.TargetHostID != serverID { // relay SDP to target host and get back sdp @@ -122,7 +122,7 @@ func (o *Server) WSO(w http.ResponseWriter, r *http.Request) { return resp } - log.Println("Target host is overlord itself: start game") + log.Println("Overlord: 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) diff --git a/webrtc/webrtc.go b/webrtc/webrtc.go index 2b52c4a1..1005c03a 100644 --- a/webrtc/webrtc.go +++ b/webrtc/webrtc.go @@ -167,20 +167,20 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e var d0 uint16 = 0 var d1 uint16 = 1 audioTrack, err := w.connection.CreateDataChannel("b", &webrtc.DataChannelInit{ - Ordered: &dfalse, + Ordered: &dfalse, MaxRetransmits: &d0, - Negotiated: &dtrue, - ID: &d1, + Negotiated: &dtrue, + ID: &d1, }) if err != nil { return "", err } // input channel - inputTrack, err := w.connection.CreateDataChannel("a", &webrtc.DataChannelInit{ - Ordered: &dtrue, + inputTrack, err := w.connection.CreateDataChannel("a", &webrtc.DataChannelInit{ + Ordered: &dtrue, Negotiated: &dtrue, - ID: &d0, + ID: &d0, }) inputTrack.OnOpen(func() { @@ -202,7 +202,6 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e w.Done <- struct{}{} close(w.Done) }) - // WebRTC state callback w.connection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { @@ -225,8 +224,6 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e log.Println(iceCandidate) }) - - offer := webrtc.SessionDescription{} Decode(remoteSession, &offer) From a104e7af96d719824ba3ffb1d28759c91e02b094 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Thu, 2 May 2019 03:55:37 +0800 Subject: [PATCH 13/23] Setup peerconnections for OverlordClient --- cmd/main_test.go | 52 +++++++++++++++++++++++++++++++++++++++++++++ handler/overlord.go | 5 +++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index 6f43b944..3e007da3 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -106,6 +106,32 @@ func initClient(t *testing.T, host string) (conn *websocket.Conn, roomID chan st return cws.EmptyPacket }) + // Request Offer routing + client.Receive("requestOffer", func(resp cws.WSPacket) cws.WSPacket { + log.Println("Frontend received requestOffer") + peerConnection, err = webrtc.NewPeerConnection(webrtcconfig) + if err != nil { + t.Fatalf("%v", err) + } + + log.Println("Recreating offer") + offer, err := peerConnection.CreateOffer(nil) + if err != nil { + t.Fatalf("%v", err) + } + + log.Println("Set localDesc") + err = peerConnection.SetLocalDescription(offer) + if err != nil { + panic(err) + } + log.Println("return offer", offer) + return cws.WSPacket{ + ID: "initwebrtc", + Data: gamertc.Encode(offer), + } + }) + time.Sleep(time.Second * 3) fmt.Println("Sending start...") @@ -252,6 +278,32 @@ func initClient2(t *testing.T, host string, remoteRoomID string) (conn *websocke return cws.EmptyPacket }) + // Request offer browser + client.Receive("requestOffer", func(resp cws.WSPacket) cws.WSPacket { + log.Println("Frontend received requestOffer") + peerConnection, err = webrtc.NewPeerConnection(webrtcconfig) + if err != nil { + t.Fatalf("%v", err) + } + + log.Println("Recreating offer") + offer, err := peerConnection.CreateOffer(nil) + if err != nil { + t.Fatalf("%v", err) + } + + log.Println("Set localDesc") + err = peerConnection.SetLocalDescription(offer) + if err != nil { + panic(err) + } + log.Println("return offer", offer) + return cws.WSPacket{ + ID: "initwebrtc", + Data: gamertc.Encode(offer), + } + }) + time.Sleep(time.Second * 3) fmt.Println("Sending start...") diff --git a/handler/overlord.go b/handler/overlord.go index 08de552e..02d0687f 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -23,7 +23,8 @@ func NewOverlordClient(oc *websocket.Conn) *OverlordClient { } oclient := &OverlordClient{ - Client: cws.NewClient(oc), + Client: cws.NewClient(oc), + peerconnections: map[string]*webrtc.WebRTC{}, } return oclient } @@ -126,9 +127,9 @@ func (s *Session) bridgeConnection(serverID string, gameName string, roomID stri 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 + log.Println("Sending offer to overlord to relay message to target host", resp.TargetHostID, "with payload", resp) remoteTargetSDP := s.OverlordClient.SyncSend(resp) log.Println("Got back remote host SDP, sending to browser") // Send back remote SDP of remote server to browser From 46598b6ef766da19e1b30a2c23fa33aaaa7f150d Mon Sep 17 00:00:00 2001 From: giongto35 Date: Thu, 2 May 2019 04:16:45 +0800 Subject: [PATCH 14/23] Fix register room bug --- cmd/main_test.go | 2 ++ handler/browser.go | 5 ++++- handler/handlers.go | 2 +- handler/overlord.go | 7 +------ 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index 3e007da3..c01f3ba3 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -336,3 +336,5 @@ func initClient2(t *testing.T, host string, remoteRoomID string) (conn *websocke // If receive roomID, the server is running correctly return ws, roomID } + +// TODO Add test reconnect room diff --git a/handler/browser.go b/handler/browser.go index 7b7817c5..a806f8a7 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -93,7 +93,10 @@ func (s *Session) RegisterBrowserClient() { // Create new room // TODO: check if roomID is in the current server - room := s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) + room := s.handler.getRoom(s.RoomID) + if room == nil { + room = s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) + } // Attach peerconnection to room room.addConnectionToRoom(s.peerconnection, s.PlayerIndex) s.RoomID = room.ID diff --git a/handler/handlers.go b/handler/handlers.go index ed50effe..7e31c0bd 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -123,7 +123,7 @@ func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) if roomID == "" || !h.isRoomRunning(roomID) { room := NewRoom(roomID, gameName) // TODO: Might have race condition - h.rooms[roomID] = room + h.rooms[room.ID] = room return room } diff --git a/handler/overlord.go b/handler/overlord.go index 02d0687f..3c7fd95a 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -83,17 +83,12 @@ func (s *Session) RegisterOverlordClient() { // Request room from Server if roomID is existed on the server room := s.handler.getRoom(s.RoomID) if room == nil { - log.Println("Room not found", s.RoomID) + log.Println("Room not found ", s.RoomID) return cws.EmptyPacket } room.addConnectionToRoom(peerconnection, s.PlayerIndex) //roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) log.Println("Done, sending back") - // Bridge always access to old room - // TODO: log warn - if room != nil { - log.Fatal("Bridge should not spawn new room") - } req.ID = "start" req.RoomID = room.ID From 6b0a02a5db62ca744c1aa348be347720e1c59cef Mon Sep 17 00:00:00 2001 From: giongto35 Date: Fri, 3 May 2019 01:49:23 +0800 Subject: [PATCH 15/23] Update test path and game path --- cmd/main_test.go | 299 ++++++++++++++++++++++++++------------------ cws/cws.go | 4 + handler/browser.go | 2 - handler/handlers.go | 19 ++- handler/overlord.go | 2 +- handler/rooms.go | 31 ++--- 6 files changed, 205 insertions(+), 152 deletions(-) diff --git a/cmd/main_test.go b/cmd/main_test.go index c01f3ba3..e52ca571 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -7,7 +7,6 @@ import ( "net/http/httptest" "strings" "testing" - "time" "github.com/giongto35/cloud-game/cws" "github.com/giongto35/cloud-game/handler" @@ -52,7 +51,7 @@ func connectTestOverlordServer(t *testing.T, overlordURL string) *websocket.Conn return oconn } -func initClient(t *testing.T, host string) (conn *websocket.Conn, roomID chan string) { +func initClient(t *testing.T, host string) (client *cws.Client) { // Convert http://127.0.0.1 to ws://127.0.0. u := "ws" + strings.TrimPrefix(host, "http") @@ -83,7 +82,7 @@ func initClient(t *testing.T, host string) (conn *websocket.Conn, roomID chan st // Send offer to server log.Println("Browser Client") - client := cws.NewClient(ws) + client = cws.NewClient(ws) go client.Listen() fmt.Println("Sending offer...") @@ -132,10 +131,19 @@ func initClient(t *testing.T, host string) (conn *websocket.Conn, roomID chan st } }) - time.Sleep(time.Second * 3) - fmt.Println("Sending start...") + return client + // If receive roomID, the server is running correctly +} - roomID = make(chan string) +func TestSingleServerNoOverlord(t *testing.T) { + // Init slave server + s := initServer(t, nil) + defer s.Close() + + client := initClient(t, s.URL) + defer client.Close() + + roomID := make(chan string) client.Send(cws.WSPacket{ ID: "start", Data: "Contra.nes", @@ -146,25 +154,12 @@ func initClient(t *testing.T, host string) (conn *websocket.Conn, roomID chan st roomID <- resp.RoomID }) - return ws, roomID - // If receive roomID, the server is running correctly -} - -func TestSingleServerNoOverlord(t *testing.T) { - // Init slave server - s := initServer(t, nil) - defer s.Close() - - conn, roomID := initClient(t, s.URL) - defer conn.Close() - respRoomID := <-roomID if respRoomID == "" { fmt.Println("RoomID should not be empty") t.Fail() } fmt.Println("Done") - conn.Close() } func TestSingleServerOneOverlord(t *testing.T) { @@ -177,14 +172,27 @@ func TestSingleServerOneOverlord(t *testing.T) { s := initServer(t, oconn) defer s.Close() - conn, roomID := initClient(t, s.URL) + client := initClient(t, s.URL) + defer client.Close() + + fmt.Println("Sending start...") + roomID := make(chan string) + client.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: "", + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + roomID <- resp.RoomID + }) + respRoomID := <-roomID if respRoomID == "" { fmt.Println("RoomID should not be empty") t.Fail() } fmt.Println("Done") - conn.Close() } func TestTwoServerOneOverlord(t *testing.T) { @@ -201,115 +209,34 @@ func TestTwoServerOneOverlord(t *testing.T) { s2 := initServer(t, oconn2) defer s2.Close() - conn1, roomID := initClient(t, s1.URL) - respRoomID := <-roomID - if respRoomID == "" { + client1 := initClient(t, s1.URL) + defer client1.Close() + + roomID := make(chan string) + client1.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: "", + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + roomID <- resp.RoomID + }) + + remoteRoomID := <-roomID + if remoteRoomID == "" { fmt.Println("RoomID should not be empty") t.Fail() } fmt.Println("Done create a room in server 1") - fmt.Println("Request the room from server 2", respRoomID) - conn2, roomID := initClient2(t, s2.URL, respRoomID) - respRoomID = <-roomID - if respRoomID == "" { - fmt.Println("RoomID should not be empty") - t.Fail() - } - - fmt.Println("Done") - conn1.Close() - conn2.Close() -} - -func initClient2(t *testing.T, host string, remoteRoomID string) (conn *websocket.Conn, roomID chan string) { - // Convert http://127.0.0.1 to ws://127.0.0. - u := "ws" + strings.TrimPrefix(host, "http") - - // Connect to the server - fmt.Println("Connecting to server") - ws, _, err := websocket.DefaultDialer.Dial(u, nil) - if err != nil { - t.Fatalf("%v", err) - } - //defer ws.Close() - - // Simulate peerconnection initialization from client - - fmt.Println("Simulating PeerConnection") - peerConnection, err := webrtc.NewPeerConnection(webrtcconfig) - if err != nil { - t.Fatalf("%v", err) - } - - offer, err := peerConnection.CreateOffer(nil) - if err != nil { - t.Fatalf("%v", err) - } - - // Sets the LocalDescription, and starts our UDP listeners - err = peerConnection.SetLocalDescription(offer) - if err != nil { - panic(err) - } - - // Send offer to server - log.Println("Browser Client") - client := cws.NewClient(ws) - go client.Listen() - - fmt.Println("Sending offer...") - client.Send(cws.WSPacket{ - ID: "initwebrtc", - Data: gamertc.Encode(offer), - }, nil) - fmt.Println("Waiting sdp...") - - client.Receive("sdp", func(resp cws.WSPacket) cws.WSPacket { - fmt.Println("received", resp.Data) - answer := webrtc.SessionDescription{} - gamertc.Decode(resp.Data, &answer) - // Apply the answer as the remote description - err = peerConnection.SetRemoteDescription(answer) - if err != nil { - panic(err) - } - - return cws.EmptyPacket - }) - - // Request offer browser - client.Receive("requestOffer", func(resp cws.WSPacket) cws.WSPacket { - log.Println("Frontend received requestOffer") - peerConnection, err = webrtc.NewPeerConnection(webrtcconfig) - if err != nil { - t.Fatalf("%v", err) - } - - log.Println("Recreating offer") - offer, err := peerConnection.CreateOffer(nil) - if err != nil { - t.Fatalf("%v", err) - } - - log.Println("Set localDesc") - err = peerConnection.SetLocalDescription(offer) - if err != nil { - panic(err) - } - log.Println("return offer", offer) - return cws.WSPacket{ - ID: "initwebrtc", - Data: gamertc.Encode(offer), - } - }) - - time.Sleep(time.Second * 3) - fmt.Println("Sending start...") - + // ------------------------------------ + // Client2 trying to create a random room and later join the the room on server1 + client2 := initClient(t, s2.URL) + defer client2.Close() // Doing the same create local room. localRoomID := make(chan string) - client.Send(cws.WSPacket{ + client2.Send(cws.WSPacket{ ID: "start", Data: "Contra.nes", RoomID: "", @@ -321,9 +248,10 @@ func initClient2(t *testing.T, host string, remoteRoomID string) (conn *websocke <-localRoomID + fmt.Println("Request the room from server 1", remoteRoomID) log.Println("Server2 trying to join server1 room") // After trying loging in to one session, login to other with the roomID - client.Send(cws.WSPacket{ + client2.Send(cws.WSPacket{ ID: "start", Data: "Contra.nes", RoomID: remoteRoomID, @@ -334,7 +262,128 @@ func initClient2(t *testing.T, host string, remoteRoomID string) (conn *websocke }) // If receive roomID, the server is running correctly - return ws, roomID + fmt.Println("Done") +} + +func TestReconnectRoomNoOverlord(t *testing.T) { + // Init slave server + s := initServer(t, nil) + + client := initClient(t, s.URL) + + fmt.Println("Sending start...") + roomID := make(chan string) + client.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: "", + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + roomID <- resp.RoomID + }) + + saveRoomID := <-roomID + if saveRoomID == "" { + fmt.Println("RoomID should not be empty") + t.Fail() + } + + log.Println("Closing room and server") + client.Close() + s.Close() + // Close server and reconnect + + // Respawn slave server + s = initServer(t, nil) + defer s.Close() + + client = initClient(t, s.URL) + defer client.Close() + + fmt.Println("Re-access room ", saveRoomID) + roomID = make(chan string) + client.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: saveRoomID, + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + roomID <- resp.RoomID + }) + + respRoomID := <-roomID + if respRoomID == "" || respRoomID != saveRoomID { + fmt.Println("The room ID should be equal to the saved room") + t.Fail() + } + + fmt.Println("Done") + } // TODO Add test reconnect room +func TestReconnectRoom(t *testing.T) { + o := initOverlord() + defer o.Close() + + oconn := connectTestOverlordServer(t, o.URL) + defer oconn.Close() + // Init slave server + s := initServer(t, oconn) + + client := initClient(t, s.URL) + + fmt.Println("Sending start...") + roomID := make(chan string) + client.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: "", + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + roomID <- resp.RoomID + }) + + saveRoomID := <-roomID + if saveRoomID == "" { + fmt.Println("RoomID should not be empty") + t.Fail() + } + + log.Println("Closing room and server") + client.Close() + s.Close() + // Close server and reconnect + + log.Println("Server respawn") + // Init slave server + s = initServer(t, oconn) + defer s.Close() + + client = initClient(t, s.URL) + defer client.Close() + + fmt.Println("Re-access room ", saveRoomID) + roomID = make(chan string) + client.Send(cws.WSPacket{ + ID: "start", + Data: "Contra.nes", + RoomID: saveRoomID, + PlayerIndex: 1, + }, func(resp cws.WSPacket) { + fmt.Println("RoomID:", resp.RoomID) + roomID <- resp.RoomID + }) + + respRoomID := <-roomID + if respRoomID == "" || respRoomID != saveRoomID { + fmt.Println("The room ID should be equal to the saved room") + t.Fail() + } + + fmt.Println("Done") + +} diff --git a/cws/cws.go b/cws/cws.go index f4a821de..2a02da42 100644 --- a/cws/cws.go +++ b/cws/cws.go @@ -163,3 +163,7 @@ func (c *Client) Listen() { } } } + +func (c *Client) Close() { + c.conn.Close() +} diff --git a/handler/browser.go b/handler/browser.go index a806f8a7..8e6d05b7 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -2,7 +2,6 @@ package handler import ( "encoding/json" - "fmt" "log" "github.com/giongto35/cloud-game/config" @@ -112,7 +111,6 @@ func (s *Session) RegisterBrowserClient() { req.ID = "start" req.RoomID = s.RoomID req.SessionID = s.ID - fmt.Println("Response from start", req) return req }) diff --git a/handler/handlers.go b/handler/handlers.go index 7e31c0bd..6ae3920a 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -121,7 +121,7 @@ func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) // or the roomID doesn't have any running sessions (room was closed) // we spawn a new room if roomID == "" || !h.isRoomRunning(roomID) { - room := NewRoom(roomID, gameName) + room := NewRoom(roomID, h.gamePath, gameName) // TODO: Might have race condition h.rooms[room.ID] = room return room @@ -129,3 +129,20 @@ func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) return nil } + +// isRoomRunning check if there is any running sessions. +// TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. +func (h *Handler) isRoomRunning(roomID string) bool { + // If no roomID is registered + if _, ok := h.rooms[roomID]; !ok { + return false + } + + // If there is running session + for _, s := range h.rooms[roomID].rtcSessions { + if !s.IsClosed() { + return true + } + } + return false +} diff --git a/handler/overlord.go b/handler/overlord.go index 3c7fd95a..f01edeaa 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -99,7 +99,7 @@ func (s *Session) RegisterOverlordClient() { } func getServerIDOfRoom(oc *OverlordClient, roomID string) string { - log.Println("Request overlord roomID") + log.Println("Request overlord roomID ", roomID) packet := oc.SyncSend( cws.WSPacket{ ID: "getRoom", diff --git a/handler/rooms.go b/handler/rooms.go index 1be88557..109a3720 100644 --- a/handler/rooms.go +++ b/handler/rooms.go @@ -29,7 +29,7 @@ type Room struct { } // init initilizes a room returns roomID -func NewRoom(roomID, gameName string) *Room { +func NewRoom(roomID, gamepath, gameName string) *Room { // if no roomID is given, generate it if roomID == "" { roomID = generateRoomID() @@ -56,7 +56,7 @@ func NewRoom(roomID, gameName string) *Room { go room.startVideo() go room.startAudio() - go director.Start([]string{"../games/" + gameName}) + go director.Start([]string{gamepath + "/" + gameName}) return room } @@ -68,23 +68,6 @@ func generateRoomID() string { return roomID } -// isRoomRunning check if there is any running sessions. -// TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. -func (h *Handler) isRoomRunning(roomID string) bool { - // If no roomID is registered - if _, ok := h.rooms[roomID]; !ok { - return false - } - - // If there is running session - for _, s := range h.rooms[roomID].rtcSessions { - if !s.IsClosed() { - return true - } - } - return false -} - func (r *Room) addConnectionToRoom(peerconnection *webrtc.WebRTC, playerIndex int) { r.cleanSession(peerconnection) peerconnection.AttachRoomID(r.ID) @@ -122,6 +105,7 @@ func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC, playerIndex int func (r *Room) cleanSession(peerconnection *webrtc.WebRTC) { r.removeSession(peerconnection) + // TODO: Clean all channels } func (r *Room) removeSession(w *webrtc.WebRTC) { @@ -130,13 +114,14 @@ func (r *Room) removeSession(w *webrtc.WebRTC) { for i, s := range r.rtcSessions { if s == w { r.rtcSessions = append(r.rtcSessions[:i], r.rtcSessions[i+1:]...) + + // If room has no sessions, close room + if len(r.rtcSessions) == 0 { + r.Done <- struct{}{} + } break } } - // If room has no sessions, close room - if len(r.rtcSessions) == 0 { - r.Done <- struct{}{} - } } func (r *Room) remove() { From 66b2b17bffd22ca4e333fa03c8dc7f7ce4833e0e Mon Sep 17 00:00:00 2001 From: giongto35 Date: Fri, 3 May 2019 02:04:32 +0800 Subject: [PATCH 16/23] Add save and load --- handler/browser.go | 63 +++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/handler/browser.go b/handler/browser.go index 8e6d05b7..022a4df1 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -36,40 +36,41 @@ func (s *Session) RegisterBrowserClient() { }) // TODO: Add save and load - //browserClient.Receive("save", func(resp cws.WSPacket) (req cws.WSPacket) { - //log.Println("Saving game state") - //req.ID = "save" - //req.Data = "ok" - //if roomID != "" { - ////err := rooms[roomID].director.SaveGame() - //err := browserClient.room.director.SaveGame() - //if err != nil { - //log.Println("[!] Cannot save game state: ", err) - //req.Data = "error" - //} - //} else { - //req.Data = "error" - //} + browserClient.Receive("save", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Saving game state") + req.ID = "save" + req.Data = "ok" + if s.RoomID != "" { + room := s.handler.getRoom(s.RoomID) + err := room.director.SaveGame() + if err != nil { + log.Println("[!] Cannot save game state: ", err) + req.Data = "error" + } + } else { + req.Data = "error" + } - //return req - //}) + return req + }) - //browserClient.Receive("load", func(resp cws.WSPacket) (req cws.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" - //} + browserClient.Receive("load", func(resp cws.WSPacket) (req cws.WSPacket) { + log.Println("Loading game state") + req.ID = "load" + req.Data = "ok" + if s.RoomID != "" { + room := s.handler.getRoom(s.RoomID) + err := room.director.LoadGame() + if err != nil { + log.Println("[!] Cannot load game state: ", err) + req.Data = "error" + } + } else { + req.Data = "error" + } - //return req - //}) + return req + }) browserClient.Receive("start", func(resp cws.WSPacket) (req cws.WSPacket) { s.GameName = resp.Data From 745b391049bec4cddc001c8209070bb9d7e8e3ac Mon Sep 17 00:00:00 2001 From: giongto35 Date: Fri, 3 May 2019 02:22:01 +0800 Subject: [PATCH 17/23] Separate room logic --- cmd/main_test.go | 106 ++++++++++++++--------------- handler/browser.go | 6 +- handler/handlers.go | 22 +++--- handler/overlord.go | 2 +- handler/{ => room}/media.go | 4 +- handler/{rooms.go => room/room.go} | 23 ++++++- 6 files changed, 88 insertions(+), 75 deletions(-) rename handler/{ => room}/media.go (96%) rename handler/{rooms.go => room/room.go} (89%) diff --git a/cmd/main_test.go b/cmd/main_test.go index e52ca571..9cdc1f2e 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -323,67 +323,67 @@ func TestReconnectRoomNoOverlord(t *testing.T) { } -// TODO Add test reconnect room -func TestReconnectRoom(t *testing.T) { - o := initOverlord() - defer o.Close() +// This test currently doesn't work +//func TestReconnectRoomWithOverlord(t *testing.T) { +//o := initOverlord() +//defer o.Close() - oconn := connectTestOverlordServer(t, o.URL) - defer oconn.Close() - // Init slave server - s := initServer(t, oconn) +//oconn := connectTestOverlordServer(t, o.URL) +//defer oconn.Close() +//// Init slave server +//s := initServer(t, oconn) - client := initClient(t, s.URL) +//client := initClient(t, s.URL) - fmt.Println("Sending start...") - roomID := make(chan string) - client.Send(cws.WSPacket{ - ID: "start", - Data: "Contra.nes", - RoomID: "", - PlayerIndex: 1, - }, func(resp cws.WSPacket) { - fmt.Println("RoomID:", resp.RoomID) - roomID <- resp.RoomID - }) +//fmt.Println("Sending start...") +//roomID := make(chan string) +//client.Send(cws.WSPacket{ +//ID: "start", +//Data: "Contra.nes", +//RoomID: "", +//PlayerIndex: 1, +//}, func(resp cws.WSPacket) { +//fmt.Println("RoomID:", resp.RoomID) +//roomID <- resp.RoomID +//}) - saveRoomID := <-roomID - if saveRoomID == "" { - fmt.Println("RoomID should not be empty") - t.Fail() - } +//saveRoomID := <-roomID +//if saveRoomID == "" { +//fmt.Println("RoomID should not be empty") +//t.Fail() +//} - log.Println("Closing room and server") - client.Close() - s.Close() - // Close server and reconnect +//log.Println("Closing room and server") +//client.Close() +//s.Close() +//// Close server and reconnect - log.Println("Server respawn") - // Init slave server - s = initServer(t, oconn) - defer s.Close() +//log.Println("Server respawn") +//// Init slave server +//s = initServer(t, oconn) +//defer s.Close() - client = initClient(t, s.URL) - defer client.Close() +//client = initClient(t, s.URL) +//defer client.Close() - fmt.Println("Re-access room ", saveRoomID) - roomID = make(chan string) - client.Send(cws.WSPacket{ - ID: "start", - Data: "Contra.nes", - RoomID: saveRoomID, - PlayerIndex: 1, - }, func(resp cws.WSPacket) { - fmt.Println("RoomID:", resp.RoomID) - roomID <- resp.RoomID - }) +//fmt.Println("Re-access room ", saveRoomID) +//roomID = make(chan string) +//client.Send(cws.WSPacket{ +//ID: "start", +//Data: "Contra.nes", +//RoomID: saveRoomID, +//PlayerIndex: 1, +//}, func(resp cws.WSPacket) { +//fmt.Println("RoomID:", resp.RoomID) +//roomID <- resp.RoomID +//}) - respRoomID := <-roomID - if respRoomID == "" || respRoomID != saveRoomID { - fmt.Println("The room ID should be equal to the saved room") - t.Fail() - } +//respRoomID := <-roomID +//if respRoomID == "" || respRoomID != saveRoomID { +//fmt.Println("The room ID should be equal to the saved room") +//t.Fail() +//} - fmt.Println("Done") +//fmt.Println("Done") -} +//} diff --git a/handler/browser.go b/handler/browser.go index 022a4df1..b05863af 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -42,7 +42,7 @@ func (s *Session) RegisterBrowserClient() { req.Data = "ok" if s.RoomID != "" { room := s.handler.getRoom(s.RoomID) - err := room.director.SaveGame() + err := room.SaveGame() if err != nil { log.Println("[!] Cannot save game state: ", err) req.Data = "error" @@ -60,7 +60,7 @@ func (s *Session) RegisterBrowserClient() { req.Data = "ok" if s.RoomID != "" { room := s.handler.getRoom(s.RoomID) - err := room.director.LoadGame() + err := room.LoadGame() if err != nil { log.Println("[!] Cannot load game state: ", err) req.Data = "error" @@ -98,7 +98,7 @@ func (s *Session) RegisterBrowserClient() { room = s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) } // Attach peerconnection to room - room.addConnectionToRoom(s.peerconnection, s.PlayerIndex) + room.AddConnectionToRoom(s.peerconnection, s.PlayerIndex) s.RoomID = room.ID // Register room to overlord if we are connecting to overlord log.Println("Try Registering room", room, "Client: ", s.OverlordClient) diff --git a/handler/handlers.go b/handler/handlers.go index 6ae3920a..675fdb2b 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -8,6 +8,7 @@ import ( "github.com/giongto35/cloud-game/cws" "github.com/giongto35/cloud-game/handler/gamelist" + "github.com/giongto35/cloud-game/handler/room" "github.com/giongto35/cloud-game/webrtc" "github.com/gorilla/websocket" uuid "github.com/satori/go.uuid" @@ -25,7 +26,7 @@ type Handler struct { // Client that connects to overlord oClient *OverlordClient // Rooms map : RoomID -> Room - rooms map[string]*Room + rooms map[string]*room.Room // ID of the current server globalwise serverID string // isDebug determines the mode handler is running @@ -42,7 +43,7 @@ func NewHandler(overlordConn *websocket.Conn, isDebug bool, gamePath string) *Ha log.Println("new OverlordClient") return &Handler{ oClient: NewOverlordClient(overlordConn), - rooms: map[string]*Room{}, + rooms: map[string]*room.Room{}, peerconnections: map[string]*webrtc.WebRTC{}, isDebug: isDebug, @@ -105,7 +106,7 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { } // getRoom returns room from roomID -func (h *Handler) getRoom(roomID string) *Room { +func (h *Handler) getRoom(roomID string) *room.Room { room, ok := h.rooms[roomID] if !ok { return nil @@ -116,12 +117,12 @@ func (h *Handler) getRoom(roomID string) *Room { // createNewRoom creates a new room // Return nil in case of room is existed -func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) *Room { +func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) *room.Room { // If the roomID is empty, // or the roomID doesn't have any running sessions (room was closed) // we spawn a new room if roomID == "" || !h.isRoomRunning(roomID) { - room := NewRoom(roomID, h.gamePath, gameName) + room := room.NewRoom(roomID, h.gamePath, gameName) // TODO: Might have race condition h.rooms[room.ID] = room return room @@ -134,15 +135,10 @@ func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) // TODO: If we remove sessions from room anytime a session is closed, we can check if the sessions list is empty or not. func (h *Handler) isRoomRunning(roomID string) bool { // If no roomID is registered - if _, ok := h.rooms[roomID]; !ok { + room, ok := h.rooms[roomID] + if !ok { return false } - // If there is running session - for _, s := range h.rooms[roomID].rtcSessions { - if !s.IsClosed() { - return true - } - } - return false + return room.IsRunning() } diff --git a/handler/overlord.go b/handler/overlord.go index f01edeaa..0ed7c4c5 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -86,7 +86,7 @@ func (s *Session) RegisterOverlordClient() { log.Println("Room not found ", s.RoomID) return cws.EmptyPacket } - room.addConnectionToRoom(peerconnection, s.PlayerIndex) + room.AddConnectionToRoom(peerconnection, s.PlayerIndex) //roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) log.Println("Done, sending back") diff --git a/handler/media.go b/handler/room/media.go similarity index 96% rename from handler/media.go rename to handler/room/media.go index 244230a7..8c9b79cf 100644 --- a/handler/media.go +++ b/handler/room/media.go @@ -1,4 +1,4 @@ -package handler +package room import ( "log" @@ -78,7 +78,6 @@ func (r *Room) startVideo() { r.remove() return case image := <-r.imageChannel: - //isRoomRunning := false yuv := util.RgbaToYuv(image) r.sessionsLock.Lock() @@ -94,7 +93,6 @@ func (r *Room) startVideo() { // NOTE: can block here webRTC.ImageChannel <- yuv } - //isRoomRunning = true } r.sessionsLock.Unlock() } diff --git a/handler/rooms.go b/handler/room/room.go similarity index 89% rename from handler/rooms.go rename to handler/room/room.go index 109a3720..4a33368a 100644 --- a/handler/rooms.go +++ b/handler/room/room.go @@ -1,4 +1,4 @@ -package handler +package room import ( "image" @@ -68,7 +68,7 @@ func generateRoomID() string { return roomID } -func (r *Room) addConnectionToRoom(peerconnection *webrtc.WebRTC, playerIndex int) { +func (r *Room) AddConnectionToRoom(peerconnection *webrtc.WebRTC, playerIndex int) { r.cleanSession(peerconnection) peerconnection.AttachRoomID(r.ID) go r.startWebRTCSession(peerconnection, playerIndex) @@ -128,3 +128,22 @@ func (r *Room) remove() { log.Println("Closing room", r) r.director.Done <- struct{}{} } + +func (r *Room) SaveGame() error { + return r.director.SaveGame() +} + +func (r *Room) LoadGame() error { + return r.director.LoadGame() +} + +func (r *Room) IsRunning() bool { + // If there is running session + for _, s := range r.rtcSessions { + if !s.IsClosed() { + return true + } + } + + return false +} From e1fe2c98db9f08ab5ad56b3df4b9152ab60388e2 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Fri, 3 May 2019 02:25:00 +0800 Subject: [PATCH 18/23] Remove un necessary comments --- handler/browser.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/handler/browser.go b/handler/browser.go index b05863af..e0f701de 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -97,13 +97,13 @@ func (s *Session) RegisterBrowserClient() { if room == nil { room = s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) } + // Attach peerconnection to room room.AddConnectionToRoom(s.peerconnection, s.PlayerIndex) s.RoomID = room.ID + // Register room to overlord if we are connecting to overlord - log.Println("Try Registering room", room, "Client: ", s.OverlordClient) if room != nil && s.OverlordClient != nil { - log.Println("Registering room", s.RoomID) s.OverlordClient.Send(cws.WSPacket{ ID: "registerRoom", Data: s.RoomID, From 8a476c7d35cc2b66e56859b97750e2f5c498ce28 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Sat, 4 May 2019 01:52:05 +0800 Subject: [PATCH 19/23] Remove session after rejoin few times --- cmd/main.go | 25 +++++++++------ emulator/gameview.go | 2 +- handler/browser.go | 2 ++ handler/cloud-storage/storage.go | 55 ++++++++++++++++++++++++++++++++ handler/handlers.go | 10 ++++++ handler/overlord.go | 1 + handler/room/media.go | 4 +-- handler/room/room.go | 21 +++++++----- webrtc/webrtc.go | 5 +++ 9 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 handler/cloud-storage/storage.go diff --git a/cmd/main.go b/cmd/main.go index 57ab7fca..c1974ff4 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -3,9 +3,11 @@ package main import ( "flag" "log" + "math/rand" "net/http" _ "net/http/pprof" "strings" + "time" "github.com/giongto35/cloud-game/config" "github.com/giongto35/cloud-game/handler" @@ -22,6 +24,15 @@ const ( // Time allowed to write a message to the peer. var upgrader = websocket.Upgrader{} +func createOverlordConnection() (*websocket.Conn, error) { + c, _, err := websocket.DefaultDialer.Dial(*config.OverlordHost, nil) + if err != nil { + return nil, err + } + + return c, nil +} + // initilizeOverlord setup an overlord server func initilizeOverlord() { overlord := overlord.NewServer() @@ -33,15 +44,6 @@ func initilizeOverlord() { http.ListenAndServe(":9000", nil) } -func createOverlordConnection() (*websocket.Conn, error) { - c, _, err := websocket.DefaultDialer.Dial(*config.OverlordHost, nil) - if err != nil { - return nil, err - } - - return c, nil -} - // initializeServer setup a server func initializeServer() { conn, err := createOverlordConnection() @@ -67,6 +69,11 @@ func main() { flag.Parse() log.Println("Usage: ./game [-debug]") + rand.Seed(time.Now().UTC().UnixNano()) + + // There are two server mode + // Overlord is coordinator. If the OvelordHost Param is `overlord`, we spawn a new host as Overlord. + // else we spawn new server as normal server connecting to OverlordHost. if *config.OverlordHost == "overlord" { log.Println("Running as overlord ") initilizeOverlord() diff --git a/emulator/gameview.go b/emulator/gameview.go index de429f2b..a6100d62 100644 --- a/emulator/gameview.go +++ b/emulator/gameview.go @@ -94,7 +94,7 @@ func (view *GameView) Enter() { view.console.SetAudioSampleRate(SampleRate) view.console.SetAudioChannel(view.audioChannel) - // load state + // load state if the hash file existed (Join the old room) if err := view.console.LoadState(savePath(view.hash)); err == nil { return } else { diff --git a/handler/browser.go b/handler/browser.go index e0f701de..d591aefd 100644 --- a/handler/browser.go +++ b/handler/browser.go @@ -94,11 +94,13 @@ func (s *Session) RegisterBrowserClient() { // Create new room // TODO: check if roomID is in the current server room := s.handler.getRoom(s.RoomID) + log.Println("Got Room from local ", room, " ID: ", s.RoomID) if room == nil { room = s.handler.createNewRoom(s.GameName, s.RoomID, s.PlayerIndex) } // Attach peerconnection to room + s.handler.detachPeerConn(s.peerconnection) room.AddConnectionToRoom(s.peerconnection, s.PlayerIndex) s.RoomID = room.ID diff --git a/handler/cloud-storage/storage.go b/handler/cloud-storage/storage.go new file mode 100644 index 00000000..15afdeed --- /dev/null +++ b/handler/cloud-storage/storage.go @@ -0,0 +1,55 @@ +package storage + +import ( + "context" + "fmt" + "io" + "log" + + "cloud.google.com/go/storage" +) + +type Client struct { + bucket string + gclient storage.Client +} + +func NewClient() *Client { + ctx := context.Background() + + // Sets your Google Cloud Platform project ID. + projectID := "YOUR_PROJECT_ID" + + // Creates a client. + client, err := storage.NewClient(ctx) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Sets the name for the new bucket. + bucketName := "my-new-bucket" + + // Creates a Bucket instance. + bucket := client.Bucket(bucketName) + + // Creates the new bucket. + if err := bucket.Create(ctx, projectID, nil); err != nil { + log.Fatalf("Failed to create bucket: %v", err) + } + + fmt.Printf("Bucket %v created.\n", bucketName) +} + +func (c *Client) SaveFile(name string, data string) (err error) { + wc := c.gclient.Bucket(c.bucket).Object(name).NewWriter(nil) + if _, err = io.Copy(wc, f); err != nil { + return err + } + if err := wc.Close(); err != nil { + return err + } +} + +func (h *Helper) LoadFile(name string) []byte { + +} diff --git a/handler/handlers.go b/handler/handlers.go index 675fdb2b..60d57e90 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -105,6 +105,16 @@ func (h *Handler) WS(w http.ResponseWriter, r *http.Request) { wssession.BrowserClient.Listen() } +// Detach peerconnection detach/remove a peerconnection from current room +func (h *Handler) detachPeerConn(pc *webrtc.WebRTC) { + roomID := pc.RoomID + room := h.getRoom(roomID) + if room == nil { + return + } + room.CleanSession(pc) +} + // getRoom returns room from roomID func (h *Handler) getRoom(roomID string) *room.Room { room, ok := h.rooms[roomID] diff --git a/handler/overlord.go b/handler/overlord.go index 0ed7c4c5..2e1ef59e 100644 --- a/handler/overlord.go +++ b/handler/overlord.go @@ -86,6 +86,7 @@ func (s *Session) RegisterOverlordClient() { log.Println("Room not found ", s.RoomID) return cws.EmptyPacket } + s.handler.detachPeerConn(s.peerconnection) room.AddConnectionToRoom(peerconnection, s.PlayerIndex) //roomID, isNewRoom := startSession(peerconnection, resp.Data, resp.RoomID, resp.PlayerIndex) log.Println("Done, sending back") diff --git a/handler/room/media.go b/handler/room/media.go index 8c9b79cf..3a3581ab 100644 --- a/handler/room/media.go +++ b/handler/room/media.go @@ -28,7 +28,7 @@ func (r *Room) startAudio() { for { select { case <-r.Done: - r.remove() + r.Close() return case sample := <-r.audioChannel: pcm[idx] = sample @@ -75,7 +75,7 @@ func (r *Room) startVideo() { for { select { case <-r.Done: - r.remove() + r.Close() return case image := <-r.imageChannel: diff --git a/handler/room/room.go b/handler/room/room.go index 4a33368a..78067e8f 100644 --- a/handler/room/room.go +++ b/handler/room/room.go @@ -1,6 +1,7 @@ package room import ( + "fmt" "image" "log" "math/rand" @@ -28,7 +29,7 @@ type Room struct { director *emulator.Director } -// init initilizes a room returns roomID +// NewRoom creates a new room func NewRoom(roomID, gamepath, gameName string) *Room { // if no roomID is given, generate it if roomID == "" { @@ -64,22 +65,21 @@ func NewRoom(roomID, gamepath, gameName string) *Room { // generateRoomID generate a unique room ID containing 16 digits func generateRoomID() string { roomID := strconv.FormatInt(rand.Int63(), 16) + log.Println("Generate Room ID", roomID) //roomID := uuid.Must(uuid.NewV4()).String() return roomID } func (r *Room) AddConnectionToRoom(peerconnection *webrtc.WebRTC, playerIndex int) { - r.cleanSession(peerconnection) peerconnection.AttachRoomID(r.ID) - go r.startWebRTCSession(peerconnection, playerIndex) - r.rtcSessions = append(r.rtcSessions, peerconnection) + + go r.startWebRTCSession(peerconnection, playerIndex) } // startWebRTCSession fan-in of the same room to inputChannel func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC, playerIndex int) { inputChannel := r.inputChannel - log.Println("room, inputChannel", r, inputChannel) for { select { case <-peerconnection.Done: @@ -103,20 +103,25 @@ func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC, playerIndex int } } -func (r *Room) cleanSession(peerconnection *webrtc.WebRTC) { +func (r *Room) CleanSession(peerconnection *webrtc.WebRTC) { r.removeSession(peerconnection) // TODO: Clean all channels } func (r *Room) removeSession(w *webrtc.WebRTC) { + fmt.Println("Cleaning session: ", w) r.sessionsLock.Lock() defer r.sessionsLock.Unlock() + fmt.Println("Sessions list", r.rtcSessions) for i, s := range r.rtcSessions { - if s == w { + fmt.Println("found session: ", s, w) + if s.ID == w.ID { r.rtcSessions = append(r.rtcSessions[:i], r.rtcSessions[i+1:]...) + fmt.Println("found session: ", len(r.rtcSessions)) // If room has no sessions, close room if len(r.rtcSessions) == 0 { + log.Println("No session in room") r.Done <- struct{}{} } break @@ -124,7 +129,7 @@ func (r *Room) removeSession(w *webrtc.WebRTC) { } } -func (r *Room) remove() { +func (r *Room) Close() { log.Println("Closing room", r) r.director.Done <- struct{}{} } diff --git a/webrtc/webrtc.go b/webrtc/webrtc.go index 1005c03a..a614ec55 100644 --- a/webrtc/webrtc.go +++ b/webrtc/webrtc.go @@ -16,6 +16,7 @@ import ( vpxEncoder "github.com/giongto35/cloud-game/vpx-encoder" "github.com/pion/webrtc" "github.com/pion/webrtc/pkg/media" + uuid "github.com/satori/go.uuid" ) var webrtcconfig = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}} @@ -94,6 +95,8 @@ func Decode(in string, obj interface{}) { // NewWebRTC create func NewWebRTC() *WebRTC { w := &WebRTC{ + ID: uuid.Must(uuid.NewV4()).String(), + ImageChannel: make(chan []byte, 2), AudioChannel: make(chan []byte, 1000), InputChannel: make(chan int, 2), @@ -108,6 +111,8 @@ type InputDataPair struct { // WebRTC connection type WebRTC struct { + ID string + connection *webrtc.PeerConnection encoder *vpxEncoder.VpxEncoder isConnected bool From 681733ac752e92c6dccb5c6bb6e02ff683f02672 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Sat, 4 May 2019 03:32:17 +0800 Subject: [PATCH 20/23] Cloud storage --- handler/cloud-storage/storage.go | 60 ++++++++++++++++++--------- handler/cloud-storage/storage_test.go | 24 +++++++++++ 2 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 handler/cloud-storage/storage_test.go diff --git a/handler/cloud-storage/storage.go b/handler/cloud-storage/storage.go index 15afdeed..f9c822ca 100644 --- a/handler/cloud-storage/storage.go +++ b/handler/cloud-storage/storage.go @@ -2,54 +2,76 @@ package storage import ( "context" - "fmt" "io" + "io/ioutil" "log" + "os" "cloud.google.com/go/storage" ) type Client struct { - bucket string - gclient storage.Client + bucket *storage.BucketHandle + gclient *storage.Client } -func NewClient() *Client { +func NewInitClient() *Client { + projectID := os.Getenv("GCP_PROJECT") + bucketName := "game-save" + return NewClient(projectID, bucketName) +} + +// NewClient inits a new Client accessing to GCP +func NewClient(projectID string, bucketName string) *Client { ctx := context.Background() // Sets your Google Cloud Platform project ID. - projectID := "YOUR_PROJECT_ID" // Creates a client. - client, err := storage.NewClient(ctx) + gclient, err := storage.NewClient(ctx) if err != nil { log.Fatalf("Failed to create client: %v", err) } - // Sets the name for the new bucket. - bucketName := "my-new-bucket" - // Creates a Bucket instance. - bucket := client.Bucket(bucketName) + bucket := gclient.Bucket(bucketName) - // Creates the new bucket. - if err := bucket.Create(ctx, projectID, nil); err != nil { - log.Fatalf("Failed to create bucket: %v", err) + return &Client{ + bucket: bucket, + gclient: gclient, } - - fmt.Printf("Bucket %v created.\n", bucketName) } -func (c *Client) SaveFile(name string, data string) (err error) { - wc := c.gclient.Bucket(c.bucket).Object(name).NewWriter(nil) - if _, err = io.Copy(wc, f); err != nil { +// Savefile save srcFile to GCP +func (c *Client) SaveFile(name string, srcFile string) (err error) { + reader, err := os.Open(srcFile) + if err != nil { + return err + } + + // Copy source file to GCP + wc := c.bucket.Object(name).NewWriter(context.Background()) + if _, err = io.Copy(wc, reader); err != nil { return err } if err := wc.Close(); err != nil { return err } + + return nil } -func (h *Helper) LoadFile(name string) []byte { +// Loadfile load file from GCP +func (c *Client) LoadFile(name string) (data []byte, err error) { + rc, err := c.bucket.Object(name).NewReader(context.Background()) + if err != nil { + return nil, err + } + defer rc.Close() + data, err = ioutil.ReadAll(rc) + if err != nil { + return nil, err + } + return data, nil } diff --git a/handler/cloud-storage/storage_test.go b/handler/cloud-storage/storage_test.go new file mode 100644 index 00000000..16e739f1 --- /dev/null +++ b/handler/cloud-storage/storage_test.go @@ -0,0 +1,24 @@ +package storage + +import ( + "io/ioutil" + "log" + "testing" +) + +func TestSaveGame(t *testing.T) { + client := NewInitClient() + data := []byte("Test Hello") + ioutil.WriteFile("/tmp/TempFile", data, 0644) + err := client.SaveFile("Test", "/tmp/TempFile") + if err != nil { + log.Panic(err) + } + loadData, err := client.LoadFile("Test") + if err != nil { + log.Panic(err) + } + if string(data) != string(loadData) { + log.Panic("Failed") + } +} From 1d2dfbf55422fac7ac733cb63945d69702c0ec04 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Sat, 4 May 2019 04:03:08 +0800 Subject: [PATCH 21/23] Save game load game to cloud --- emulator/director.go | 14 +++++++- handler/cloud-storage/storage.go | 1 + handler/handlers.go | 8 ++++- handler/room/room.go | 58 ++++++++++++++++++++++++++------ 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/emulator/director.go b/emulator/director.go index ce3a3ea4..29696885 100644 --- a/emulator/director.go +++ b/emulator/director.go @@ -19,7 +19,9 @@ type Director struct { Done chan struct{} roomID string - hash string + // Hash represents a game state (roomID, gamePath). + // It is used for save file name + hash string } const FPS = 60 @@ -126,3 +128,13 @@ func (d *Director) LoadGame() error { return nil } } + +// GetHash return hash +func (d *Director) GetHash() string { + return d.hash +} + +// GetHashPath return the full path to hash file +func (d *Director) GetHashPath() string { + return savePath(d.hash) +} diff --git a/handler/cloud-storage/storage.go b/handler/cloud-storage/storage.go index f9c822ca..3aa0d6c4 100644 --- a/handler/cloud-storage/storage.go +++ b/handler/cloud-storage/storage.go @@ -10,6 +10,7 @@ import ( "cloud.google.com/go/storage" ) +// TODO: Add interface, abstract out Gstorage type Client struct { bucket *storage.BucketHandle gclient *storage.Client diff --git a/handler/handlers.go b/handler/handlers.go index 60d57e90..2d444d2b 100644 --- a/handler/handlers.go +++ b/handler/handlers.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/giongto35/cloud-game/cws" + storage "github.com/giongto35/cloud-game/handler/cloud-storage" "github.com/giongto35/cloud-game/handler/gamelist" "github.com/giongto35/cloud-game/handler/room" "github.com/giongto35/cloud-game/webrtc" @@ -36,11 +37,14 @@ type Handler struct { // All webrtc peerconnections are handled by the server // ID -> peerconnections peerconnections map[string]*webrtc.WebRTC + // onlineStorage is client accessing to online storage (GCP) + onlineStorage *storage.Client } // NewHandler returns a new server func NewHandler(overlordConn *websocket.Conn, isDebug bool, gamePath string) *Handler { log.Println("new OverlordClient") + return &Handler{ oClient: NewOverlordClient(overlordConn), rooms: map[string]*room.Room{}, @@ -48,6 +52,8 @@ func NewHandler(overlordConn *websocket.Conn, isDebug bool, gamePath string) *Ha isDebug: isDebug, gamePath: gamePath, + + onlineStorage: storage.NewInitClient(), } } @@ -132,7 +138,7 @@ func (h *Handler) createNewRoom(gameName string, roomID string, playerIndex int) // or the roomID doesn't have any running sessions (room was closed) // we spawn a new room if roomID == "" || !h.isRoomRunning(roomID) { - room := room.NewRoom(roomID, h.gamePath, gameName) + room := room.NewRoom(roomID, h.gamePath, gameName, h.onlineStorage) // TODO: Might have race condition h.rooms[room.ID] = room return room diff --git a/handler/room/room.go b/handler/room/room.go index 78067e8f..ace0c589 100644 --- a/handler/room/room.go +++ b/handler/room/room.go @@ -3,12 +3,15 @@ package room import ( "fmt" "image" + "io/ioutil" "log" "math/rand" "strconv" "sync" + "time" emulator "github.com/giongto35/cloud-game/emulator" + storage "github.com/giongto35/cloud-game/handler/cloud-storage" "github.com/giongto35/cloud-game/webrtc" ) @@ -27,10 +30,13 @@ type Room struct { sessionsLock *sync.Mutex director *emulator.Director + + // Cloud storage to store room state online + onlineStorage *storage.Client } // NewRoom creates a new room -func NewRoom(roomID, gamepath, gameName string) *Room { +func NewRoom(roomID, gamepath, gameName string, onlineStorage *storage.Client) *Room { // if no roomID is given, generate it if roomID == "" { roomID = generateRoomID() @@ -46,13 +52,14 @@ func NewRoom(roomID, gamepath, gameName string) *Room { room := &Room{ ID: roomID, - imageChannel: imageChannel, - audioChannel: audioChannel, - inputChannel: inputChannel, - rtcSessions: []*webrtc.WebRTC{}, - sessionsLock: &sync.Mutex{}, - director: director, - Done: make(chan struct{}), + imageChannel: imageChannel, + audioChannel: audioChannel, + inputChannel: inputChannel, + rtcSessions: []*webrtc.WebRTC{}, + sessionsLock: &sync.Mutex{}, + director: director, + Done: make(chan struct{}), + onlineStorage: onlineStorage, } go room.startVideo() @@ -135,11 +142,42 @@ func (r *Room) Close() { } func (r *Room) SaveGame() error { - return r.director.SaveGame() + // TODO: Move to game view + if err := r.director.SaveGame(); err != nil { + return err + } + time.Sleep(1000 * time.Millisecond) + + // Try to save the game to gCloud + // TODO: Add goroutine + if err := r.onlineStorage.SaveFile(r.director.GetHash(), r.director.GetHashPath()); err != nil { + return err + } + + return nil } func (r *Room) LoadGame() error { - return r.director.LoadGame() + err := r.director.LoadGame() + time.Sleep(1000 * time.Millisecond) + if err != nil { + // TODO: Put in GoRoutine + // If the game is not on local server + // Try to load from gcloud + data, err := r.onlineStorage.LoadFile(r.director.GetHash()) + if err != nil { + return err + } + // Save the data fetched from gcloud to local server + ioutil.WriteFile(r.director.GetHashPath(), data, 0644) + // Reload game again + err = r.director.LoadGame() + if err != nil { + return err + } + } + + return err } func (r *Room) IsRunning() bool { From e79e65511263c11c9cd5e417b9e08dc4f3f49739 Mon Sep 17 00:00:00 2001 From: giongto35 Date: Sat, 4 May 2019 17:33:53 +0800 Subject: [PATCH 22/23] Save load from cloud --- emulator/director.go | 10 ++++++---- emulator/gameview.go | 41 +++++++++++++++++++++++++++++------------ handler/room/room.go | 35 +++++++++++++++++++---------------- 3 files changed, 54 insertions(+), 32 deletions(-) diff --git a/emulator/director.go b/emulator/director.go index 29696885..770e5227 100644 --- a/emulator/director.go +++ b/emulator/director.go @@ -111,18 +111,20 @@ func (d *Director) PlayGame(path string) { d.SetView(NewGameView(console, path, hash, d.imageChannel, d.audioChannel, d.inputChannel)) } -func (d *Director) SaveGame() error { +// SaveGame creates save events and doing extra step for load +func (d *Director) SaveGame(saveExtraFunc func() error) error { if d.hash != "" { - d.view.Save(d.hash) + d.view.Save(d.hash, saveExtraFunc) return nil } else { return nil } } -func (d *Director) LoadGame() error { +// LoadGame creates load events and doing extra step for load +func (d *Director) LoadGame(loadExtraFunc func() error) error { if d.hash != "" { - d.view.Load(d.hash) + d.view.Load(d.hash, loadExtraFunc) return nil } else { return nil diff --git a/emulator/gameview.go b/emulator/gameview.go index a6100d62..c9e96c17 100644 --- a/emulator/gameview.go +++ b/emulator/gameview.go @@ -3,6 +3,7 @@ package emulator import ( "image" + "log" "github.com/giongto35/cloud-game/emulator/nes" ) @@ -45,14 +46,19 @@ type GameView struct { // equivalent to the list key pressed const above keyPressed [NumKeys * 2]bool - savingPath string - loadingPath string + savingJob *job + loadingJob *job imageChannel chan *image.RGBA audioChannel chan float32 inputChannel chan int } +type job struct { + path string + extraFunc func() error +} + func NewGameView(console *nes.Console, title, hash string, imageChannel chan *image.RGBA, audioChannel chan float32, inputChannel chan int) *GameView { gameview := &GameView{ console: console, @@ -136,26 +142,37 @@ func (view *GameView) Update(t, dt float64) { view.imageChannel <- console.Buffer() } -func (view *GameView) Save(hash string) { +func (view *GameView) Save(hash string, extraSaveFunc func() error) { // put saving event to queue, process in updateEvent - view.savingPath = savePath(view.hash) + view.savingJob = &job{ + path: savePath(view.hash), + extraFunc: extraSaveFunc, + } } -func (view *GameView) Load(path string) { +func (view *GameView) Load(path string, extraLoadFunc func() error) { // put saving event to queue, process in updateEvent - view.loadingPath = savePath(view.hash) + view.loadingJob = &job{ + path: savePath(view.hash), + extraFunc: extraLoadFunc, + } } 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 = "" + log.Println(view.savingJob) + if view.savingJob != nil { + view.console.SaveState(view.savingJob.path) + // Run extra function (online saving for example) + go view.savingJob.extraFunc() + view.savingJob = nil } // If there is loading event, save and discard the load event - if view.loadingPath != "" { - view.console.LoadState(view.loadingPath) - view.loadingPath = "" + if view.loadingJob != nil { + view.console.LoadState(view.loadingJob.path) + // Run extra function (online saving for example) + go view.loadingJob.extraFunc() + view.loadingJob = nil } } diff --git a/handler/room/room.go b/handler/room/room.go index ace0c589..6f35f8e6 100644 --- a/handler/room/room.go +++ b/handler/room/room.go @@ -8,7 +8,6 @@ import ( "math/rand" "strconv" "sync" - "time" emulator "github.com/giongto35/cloud-game/emulator" storage "github.com/giongto35/cloud-game/handler/cloud-storage" @@ -142,15 +141,18 @@ func (r *Room) Close() { } func (r *Room) SaveGame() error { - // TODO: Move to game view - if err := r.director.SaveGame(); err != nil { - return err - } - time.Sleep(1000 * time.Millisecond) + onlineSaveFunc := func() error { + // Try to save the game to gCloud + // TODO: Add goroutine + if err := r.onlineStorage.SaveFile(r.director.GetHash(), r.director.GetHashPath()); err != nil { + return err + } - // Try to save the game to gCloud - // TODO: Add goroutine - if err := r.onlineStorage.SaveFile(r.director.GetHash(), r.director.GetHashPath()); err != nil { + return nil + } + + // TODO: Move to game view + if err := r.director.SaveGame(onlineSaveFunc); err != nil { return err } @@ -158,9 +160,7 @@ func (r *Room) SaveGame() error { } func (r *Room) LoadGame() error { - err := r.director.LoadGame() - time.Sleep(1000 * time.Millisecond) - if err != nil { + onlineLoadFunc := func() error { // TODO: Put in GoRoutine // If the game is not on local server // Try to load from gcloud @@ -171,12 +171,15 @@ func (r *Room) LoadGame() error { // Save the data fetched from gcloud to local server ioutil.WriteFile(r.director.GetHashPath(), data, 0644) // Reload game again - err = r.director.LoadGame() - if err != nil { - return err - } + //err = r.director.LoadGame(nil) + //if err != nil { + //return err + //} + return nil } + err := r.director.LoadGame(onlineLoadFunc) + return err } From cf4dadc1d90d2ef822c34d90e697fa5d84cc19db Mon Sep 17 00:00:00 2001 From: giongto35 Date: Sat, 4 May 2019 17:52:46 +0800 Subject: [PATCH 23/23] Update comments --- emulator/gameview.go | 2 -- handler/room/room.go | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/emulator/gameview.go b/emulator/gameview.go index c9e96c17..28051fa8 100644 --- a/emulator/gameview.go +++ b/emulator/gameview.go @@ -3,7 +3,6 @@ package emulator import ( "image" - "log" "github.com/giongto35/cloud-game/emulator/nes" ) @@ -160,7 +159,6 @@ func (view *GameView) Load(path string, extraLoadFunc func() error) { func (view *GameView) UpdateEvents() { // If there is saving event, save and discard the save event - log.Println(view.savingJob) if view.savingJob != nil { view.console.SaveState(view.savingJob.path) // Run extra function (online saving for example) diff --git a/handler/room/room.go b/handler/room/room.go index 6f35f8e6..2797a755 100644 --- a/handler/room/room.go +++ b/handler/room/room.go @@ -143,7 +143,6 @@ func (r *Room) Close() { func (r *Room) SaveGame() error { onlineSaveFunc := func() error { // Try to save the game to gCloud - // TODO: Add goroutine if err := r.onlineStorage.SaveFile(r.director.GetHash(), r.director.GetHashPath()); err != nil { return err } @@ -160,8 +159,9 @@ func (r *Room) SaveGame() error { } func (r *Room) LoadGame() error { + // TODO: Fix, because load game always come to local, this logic is unnecessary. Move to load game onlineLoadFunc := func() error { - // TODO: Put in GoRoutine + log.Println("Loading game from cloud storage") // If the game is not on local server // Try to load from gcloud data, err := r.onlineStorage.LoadFile(r.director.GetHash())