From 74b66ae6eabbdfc0e76cc47432f3f848c6ebab45 Mon Sep 17 00:00:00 2001 From: Nguyen Huu Thanh Date: Tue, 30 Apr 2019 17:33:35 +0700 Subject: [PATCH] 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 }