mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-07-21 09:08:57 +00:00
Remove HTTP
This commit is contained in:
parent
f27f03144f
commit
9bde5b14dd
5 changed files with 25 additions and 447 deletions
137
main.go
137
main.go
|
|
@ -1,31 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
pionRTC "github.com/pion/webrtc"
|
||||
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/cloud-game/ui"
|
||||
"github.com/giongto35/cloud-game/util"
|
||||
"github.com/giongto35/cloud-game/webrtc"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"encoding/json"
|
||||
pionRTC "github.com/pion/webrtc"
|
||||
)
|
||||
|
||||
const gameboyIndex = "./static/gameboy.html"
|
||||
const httpIndex = "./static/index_http.html"
|
||||
const wsIndex = "./static/index_ws.html"
|
||||
|
||||
var width = 256
|
||||
|
|
@ -50,12 +44,6 @@ type WSPacket struct {
|
|||
PlayerIndex int `json:"player_index"`
|
||||
}
|
||||
|
||||
type SessionPacket struct {
|
||||
Game string `json:"game"`
|
||||
RoomID string `json:"room_id"`
|
||||
SDP string `json:"sdp"`
|
||||
}
|
||||
|
||||
type Room struct {
|
||||
imageChannel chan *image.RGBA
|
||||
inputChannel chan int
|
||||
|
|
@ -64,54 +52,33 @@ type Room struct {
|
|||
|
||||
var rooms map[string]*Room
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func startGame(path string, roomID string, imageChannel chan *image.RGBA, inputChannel chan int) {
|
||||
ui.Run([]string{path}, roomID, imageChannel, inputChannel)
|
||||
}
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
fmt.Printf("Usage: %s [ws]\n", os.Args[0])
|
||||
fmt.Println("http://localhost:8000")
|
||||
rooms = map[string]*Room{}
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
service = "ws"
|
||||
log.Println("Using websocket")
|
||||
|
||||
// ignore origin
|
||||
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
|
||||
http.HandleFunc("/ws", ws)
|
||||
} else {
|
||||
log.Println("Using http")
|
||||
http.HandleFunc("/session", postSession)
|
||||
}
|
||||
// ignore origin
|
||||
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
|
||||
http.HandleFunc("/ws", ws)
|
||||
|
||||
http.HandleFunc("/", getWeb)
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
|
||||
http.ListenAndServe(":8000", nil)
|
||||
}
|
||||
|
||||
func getWeb(w http.ResponseWriter, r *http.Request) {
|
||||
if indexFN != gameboyIndex {
|
||||
bs, err := ioutil.ReadFile(indexFN)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
w.Write(bs)
|
||||
} else {
|
||||
// gameboy index
|
||||
tmpl := template.Must(template.ParseFiles(indexFN))
|
||||
data := IndexPageData{
|
||||
Service: service,
|
||||
}
|
||||
tmpl.Execute(w, data)
|
||||
}
|
||||
func startGame(path string, roomID string, imageChannel chan *image.RGBA, inputChannel chan int) {
|
||||
ui.Run([]string{path}, roomID, imageChannel, inputChannel)
|
||||
}
|
||||
|
||||
// initRoom initilize room returns roomID
|
||||
func getWeb(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := ioutil.ReadFile(indexFN)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
// init initilize room returns roomID
|
||||
func initRoom(roomID, gameName string) string {
|
||||
roomID = generateRoomID()
|
||||
imageChannel := make(chan *image.RGBA, 100)
|
||||
|
|
@ -127,7 +94,9 @@ func initRoom(roomID, gameName string) string {
|
|||
return roomID
|
||||
}
|
||||
|
||||
// startSession handles one session call
|
||||
func startSession(webRTC *webrtc.WebRTC, gameName string, roomID string, playerIndex int) string {
|
||||
// If the roomID is empty, we spawn a new room
|
||||
if roomID == "" {
|
||||
roomID = initRoom(roomID, gameName)
|
||||
}
|
||||
|
|
@ -168,15 +137,15 @@ func ws(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
req := WSPacket{}
|
||||
res := WSPacket{}
|
||||
|
||||
err = json.Unmarshal(message, &req)
|
||||
if err != nil {
|
||||
log.Println("[!] json unmarshal:", err)
|
||||
break
|
||||
}
|
||||
// log.Println(req)
|
||||
|
||||
// connectivity
|
||||
res := WSPacket{}
|
||||
switch req.ID {
|
||||
case "ping":
|
||||
gameName = req.Data
|
||||
|
|
@ -228,64 +197,6 @@ func ws(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func postSession(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
var postPacket SessionPacket
|
||||
err = json.Unmarshal(bs, &postPacket)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
log.Println("Got session with game request:", postPacket.Game)
|
||||
|
||||
webRTC := webrtc.NewWebRTC()
|
||||
|
||||
localSession, err := webRTC.StartClient(postPacket.SDP, width, height)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
roomID := postPacket.RoomID
|
||||
if roomID == "" {
|
||||
fmt.Println("Init Room")
|
||||
//generate new room
|
||||
roomID = generateRoomID()
|
||||
|
||||
imageChannel := make(chan *image.RGBA, 100)
|
||||
inputChannel := make(chan int, 100)
|
||||
rooms[roomID] = &Room{
|
||||
imageChannel: imageChannel,
|
||||
inputChannel: inputChannel,
|
||||
rtcSessions: []*webrtc.WebRTC{},
|
||||
}
|
||||
go fanoutScreen(imageChannel, roomID)
|
||||
go startGame("games/"+postPacket.Game, roomID, imageChannel, inputChannel)
|
||||
// fanin input channel
|
||||
// fanout output channel
|
||||
} else {
|
||||
// if there is room, reuse image channel, add webRTC session
|
||||
}
|
||||
rooms[roomID].rtcSessions = append(rooms[roomID].rtcSessions, webRTC)
|
||||
faninInput(rooms[roomID].inputChannel, webRTC, 1)
|
||||
|
||||
res := SessionPacket{
|
||||
SDP: localSession,
|
||||
RoomID: roomID,
|
||||
}
|
||||
stRes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
log.Println("json marshal:", err)
|
||||
}
|
||||
|
||||
//w.Write([]byte(localSession))
|
||||
w.Write(stRes)
|
||||
}
|
||||
|
||||
// generateRoomID generate a unique room ID containing 16 digits
|
||||
func generateRoomID() string {
|
||||
roomID := strconv.FormatInt(rand.Int63(), 16)
|
||||
|
|
|
|||
|
|
@ -176,5 +176,5 @@
|
|||
|
||||
<script src="/static/js/gameboy_loader.js"></script>
|
||||
<script src="/static/js/gameboy_controller.js"></script>
|
||||
<script src="/static/js/{{.Service}}.js"></script>
|
||||
<script src="/static/js/ws.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,245 +0,0 @@
|
|||
<html>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
width: 60%;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<select id="gameOp">
|
||||
<option value="Contra.nes">Contra.nes</option>
|
||||
<option value="Kirby's Adventure.nes">Kirby's Adventure.nes</option>
|
||||
<option value="Mega Man 2.nes">Mega Man 2.nes</option>
|
||||
<option value="Metal Gear.nes">Metal Gear.nes</option>
|
||||
<option value="Mortal Kombat 4.nes">Mortal Kombat 4.nes</option>
|
||||
<option value="Super Mario Bros 2.nes">Super Mario Bros 2.nes</option>
|
||||
<option value="Super Mario Bros 3.nes">Super Mario Bros 3.nes</option>
|
||||
<option value="Super Mario Bros.nes">Super Mario Bros.nes</option>
|
||||
<option value="Teenage Mutant Ninja Turtles 3.nes">Teenage Mutant Ninja Turtles 3.nes</option>
|
||||
<option value="VS Super Mario Bros.nes">VS Super Mario Bros.nes</option>
|
||||
<option value="supermariobros.rom">supermariobros.rom</option>
|
||||
<option value="zelda.rom">zelda.rom</option>
|
||||
</select>
|
||||
<button id="playGame" onclick="window.startSession()">Play Mario</button>
|
||||
|
||||
Room ID: <input type="text" id="roomID"><br>
|
||||
|
||||
<br/><br/>
|
||||
|
||||
<body scroll="no" style="overflow: hidden">
|
||||
<div id="remoteVideos" ></div> <br />
|
||||
|
||||
<h3>Instruction</h3>
|
||||
<div>
|
||||
Use Up, Down, Left, Right to Move <br />
|
||||
Z to jump (A) <br />
|
||||
X to sprint (B) <br />
|
||||
C is start button <br />
|
||||
V is select button <br />
|
||||
|
||||
<!--Fullscreen media for better gaming experience<br /-->
|
||||
</div><br>
|
||||
|
||||
<h3>Log:</h3>
|
||||
<pre id="div"></pre>
|
||||
|
||||
<div>
|
||||
🎮<u><i>Refresh to retry when checking is too long</i></u>
|
||||
</div>
|
||||
<script>
|
||||
|
||||
// miscs
|
||||
DEBUG = true;
|
||||
|
||||
let log = msg => {
|
||||
if (DEBUG) {
|
||||
document.getElementById('div').innerHTML += msg + '<br>'
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var localSessionDescription = ""
|
||||
var remoteSessionDescription = ""
|
||||
var roomID = ""
|
||||
|
||||
// Register with server the session description
|
||||
function postSession(session) {
|
||||
if (session == "") {
|
||||
return;
|
||||
}
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
d = JSON.parse(this.responseText);
|
||||
remoteSessionDescription = d["sdp"]
|
||||
document.getElementById('roomID').value = d["room_id"]
|
||||
document.getElementById('playGame').disabled = false;
|
||||
}
|
||||
};
|
||||
|
||||
roomID = document.getElementById('roomID').value
|
||||
xhttp.open("POST", "/session", true);
|
||||
xhttp.setRequestHeader("Content-type", "application/json");
|
||||
xhttp.send(JSON.stringify({"game": gameOp.value, "room_id": roomID, "sdp": session}));
|
||||
|
||||
}
|
||||
let pc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const dataChannelOptions = {
|
||||
ordered: false, // do not guarantee order
|
||||
maxPacketLifeTime: 3000, // in milliseconds
|
||||
};
|
||||
let inputChannel = pc.createDataChannel('foo')
|
||||
inputChannel.onclose = () => console.log('inputChannel has closed')
|
||||
inputChannel.onopen = () => console.log('inputChannel has opened')
|
||||
inputChannel.onmessage = e => log(`Message from DataChannel '${inputChannel.label}' payload '${e.data}'`)
|
||||
|
||||
pc.ontrack = function (event) {
|
||||
var el = document.createElement(event.track.kind)
|
||||
el.srcObject = event.streams[0]
|
||||
el.autoplay = true
|
||||
el.width = 800;
|
||||
el.height = 600;
|
||||
el.poster = new URL("https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif");
|
||||
|
||||
document.getElementById('remoteVideos').appendChild(el)
|
||||
}
|
||||
|
||||
pc.onicecandidate = event => {
|
||||
if (event.candidate === null) {
|
||||
var session = btoa(JSON.stringify(pc.localDescription));
|
||||
localSessionDescription = session;
|
||||
console.log("ice candidate")
|
||||
postSession(session)
|
||||
}
|
||||
}
|
||||
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).then(d => pc.setLocalDescription(d)).catch(log)
|
||||
// Key controller
|
||||
keyState = {
|
||||
// controllers
|
||||
a: false,
|
||||
b: false,
|
||||
start: false,
|
||||
select: false,
|
||||
|
||||
// navigators
|
||||
up: false,
|
||||
down: false,
|
||||
left: false,
|
||||
right: false,
|
||||
}
|
||||
|
||||
keyMap = {
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
|
||||
90: "a",
|
||||
88: "b",
|
||||
67: "start",
|
||||
86: "select",
|
||||
}
|
||||
|
||||
INPUT_FPS = 100;
|
||||
INPUT_STATE_PACKET = 5;
|
||||
|
||||
stateUnchange = true;
|
||||
unchangePacket = INPUT_STATE_PACKET;
|
||||
|
||||
function setState(e, bo) {
|
||||
if (e.keyCode in keyMap) {
|
||||
keyState[keyMap[e.keyCode]] = bo;
|
||||
stateUnchange = false;
|
||||
unchangePacket = INPUT_STATE_PACKET;
|
||||
}
|
||||
}
|
||||
|
||||
document.body.onkeydown = function(e){
|
||||
setState(e, true);
|
||||
};
|
||||
|
||||
document.body.onkeyup = function(e){
|
||||
setState(e, false);
|
||||
};
|
||||
|
||||
window.startSession = () => {
|
||||
console.log("Start session")
|
||||
let sd = remoteSessionDescription
|
||||
if (sd === '') {
|
||||
return alert('Session Description must not be empty')
|
||||
}
|
||||
try {
|
||||
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))));
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}
|
||||
|
||||
var timer = null;
|
||||
function sendInput() {
|
||||
// prepare key
|
||||
/*
|
||||
const (
|
||||
ButtonA = iota
|
||||
ButtonB
|
||||
ButtonSelect
|
||||
ButtonStart
|
||||
ButtonUp
|
||||
ButtonDown
|
||||
ButtonLeft
|
||||
ButtonRight
|
||||
)
|
||||
*/
|
||||
|
||||
if (stateUnchange || unchangePacket > 0) {
|
||||
st = "";
|
||||
["a", "b", "select", "start", "up", "down", "left", "right"].forEach(elem => {
|
||||
st += keyState[elem]?1:0;
|
||||
});
|
||||
ss = parseInt(st, 2);
|
||||
console.log(`Key state string: ${st} ==> ${ss}`);
|
||||
|
||||
// send
|
||||
inputChannel.send(ss);
|
||||
|
||||
stateUnchange = false;
|
||||
unchangePacket--;
|
||||
}
|
||||
}
|
||||
|
||||
function startInput() {
|
||||
if (timer == null) {
|
||||
timer = setInterval(sendInput, 1000 / INPUT_FPS)
|
||||
}
|
||||
}
|
||||
|
||||
function endInput() {
|
||||
clearInterval(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
log(pc.iceConnectionState);
|
||||
if (pc.iceConnectionState === "connected") {
|
||||
startInput();
|
||||
}
|
||||
else if (pc.iceConnectionState === "disconnected") {
|
||||
endInput();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
</html>
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
// http signal server
|
||||
|
||||
function startGame() {
|
||||
log("Starting game screen")
|
||||
|
||||
// clear
|
||||
endInput();
|
||||
document.getElementById('div').innerHTML = "";
|
||||
$("#loading-screen").show();
|
||||
$("#menu-screen").fadeOut();
|
||||
// end clear
|
||||
|
||||
// Register with server the session description
|
||||
function postSession(session) {
|
||||
if (session == "") {
|
||||
return;
|
||||
}
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function () {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
remoteSessionDescription = this.responseText;
|
||||
// document.getElementById('playGame').disabled = false;
|
||||
// by original design, we would click to start.
|
||||
startSession();
|
||||
}
|
||||
};
|
||||
xhttp.open("POST", "/session", true);
|
||||
xhttp.setRequestHeader("Content-type", "application/json");
|
||||
xhttp.send(JSON.stringify({ "game": GAME_LIST[gameIdx].nes, "sdp": session }));
|
||||
|
||||
}
|
||||
let pc = new RTCPeerConnection({
|
||||
iceServers: [
|
||||
{
|
||||
urls: 'stun:stun.l.google.com:19302'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
inputChannel = pc.createDataChannel('foo')
|
||||
inputChannel.onclose = () => log('inputChannel has closed')
|
||||
inputChannel.onopen = () => log('inputChannel has opened')
|
||||
inputChannel.onmessage = e => log(`Message from DataChannel '${inputChannel.label}' payload '${e.data}'`)
|
||||
|
||||
pc.ontrack = function (event) {
|
||||
log("New stream, yay!");
|
||||
document.getElementById("loading-screen").srcObject = event.streams[0];
|
||||
|
||||
// document.getElementById('remoteVideos').appendChild(el)
|
||||
}
|
||||
|
||||
pc.onicecandidate = event => {
|
||||
if (event.candidate === null) {
|
||||
var session = btoa(JSON.stringify(pc.localDescription));
|
||||
localSessionDescription = session;
|
||||
postSession(session)
|
||||
}
|
||||
}
|
||||
pc.createOffer({ offerToReceiveVideo: true, offerToReceiveAudio: true }).then(d => pc.setLocalDescription(d)).catch(log)
|
||||
|
||||
window.startSession = () => {
|
||||
let sd = remoteSessionDescription
|
||||
if (sd === '') {
|
||||
return alert('Session Description must not be empty')
|
||||
}
|
||||
try {
|
||||
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))));
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}
|
||||
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
log(`iceConnectionState: ${pc.iceConnectionState}`);
|
||||
|
||||
if (pc.iceConnectionState === "connected") {
|
||||
startInput();
|
||||
screenState = "game";
|
||||
}
|
||||
else if (pc.iceConnectionState === "disconnected") {
|
||||
endInput();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -81,8 +81,6 @@ function startGame() {
|
|||
pc.ontrack = function (event) {
|
||||
log("New stream, yay!");
|
||||
document.getElementById("loading-screen").srcObject = event.streams[0];
|
||||
// document.getElementById("loading-screen").width = 270;
|
||||
// document.getElementById("loading-screen").height = 240;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue