Add Room logics

This commit is contained in:
giongto35 2019-04-07 18:54:34 +08:00
commit 0abbe46c1b
28 changed files with 7595 additions and 22 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

48
main.go
View file

@ -1,6 +1,7 @@
package main
import (
"html/template"
"math/rand"
"os"
"strconv"
@ -18,7 +19,6 @@ import (
"github.com/giongto35/cloud-game/util"
"github.com/giongto35/cloud-game/webrtc"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"encoding/json"
@ -27,7 +27,12 @@ import (
// var webRTC *webrtc.WebRTC
var width = 256
var height = 240
var index string = "./index_http.html"
var indexFN string = "./static/gameboy.html"
var service string = "http"
type IndexPageData struct {
Service string
}
var upgrader = websocket.Upgrader{}
@ -37,6 +42,12 @@ type WSPacket struct {
RoomID string `json:"room_id"`
}
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
@ -57,30 +68,35 @@ func main() {
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")
index = "./index_ws.html"
// ignore origin
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
http.HandleFunc("/", getWeb)
http.HandleFunc("/ws", ws)
http.ListenAndServe(":8000", nil)
} else {
log.Println("Using http")
router := mux.NewRouter()
router.HandleFunc("/", getWeb).Methods("GET")
router.HandleFunc("/session", postSession).Methods("POST")
http.ListenAndServe(":8000", router)
http.HandleFunc("/session", postSession)
}
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) {
bs, err := ioutil.ReadFile(index)
if err != nil {
log.Fatal(err)
tmpl := template.Must(template.ParseFiles(indexFN))
data := IndexPageData{
Service: service,
}
w.Write(bs)
tmpl.Execute(w, data)
// bs, err := ioutil.ReadFile(index)
// if err != nil {
// log.Fatal(err)
// }
// w.Write(bs)
}
// initRoom initilize room returns roomID
@ -195,12 +211,6 @@ func ws(w http.ResponseWriter, r *http.Request) {
}
}
type SessionPacket struct {
Game string `json:"game"`
RoomID string `json:"room_id"`
SDP string `json:"sdp"`
}
func postSession(w http.ResponseWriter, r *http.Request) {
bs, err := ioutil.ReadAll(r.Body)
if err != nil {

6922
static/css/gameboy.css Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

161
static/gameboy.html Normal file
View file

@ -0,0 +1,161 @@
<head>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
<link type="text/css" rel="stylesheet" href="/static/css/gameboy.css">
</head>
<body>
<div id="gameboy" class="green">
<div id="canvas"></div>
<div id="border"></div>
<div id="border-top"></div>
<div id="border-left"></div>
<div id="border-bottom"></div>
<div id="border-right"></div>
<div id="screw-small-right" class="screw small"></div>
<div id="screw-small-left" class="screw small"></div>
<div id="screw-large-right" class="screw large"></div>
<div id="screw-large-left" class="screw large"></div>
<div id="backboard"></div>
<div id="motherboard"></div>
<div id="motherboard-capacitors" class="capacitors"></div>
<div id="chip-short" class="chip"></div>
<div id="chip-diagonal" class="chip"></div>
<div id="chip-tall" class="chip"></div>
<div id="chip-capacitors" class="capacitors"></div>
<div id="contrast-knob"></div>
<div id="link-port"></div>
<div id="circuit-bottom" class="circuit"></div>
<div id="circuit-top" class="circuit"></div>
<div id="transistors"></div>
<div id="processor"></div>
<div id="component"></div>
<div id="controller"></div>
<div id="speaker"></div>
<div id="whitescreen"></div>
<div id="glass"></div>
<div id="glass-gameboy-text">GAME BOY</div>
<div id="glass-color-text">C</div>
<div id="screen">
<video id="loading-screen" autoplay=true poster="https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif">
</video>
<div id="menu-screen">
<img id="box-art" src="/static/img/boxarts/Contra (USA).png">
<div id="title">
<p>Contra</p>
</div>
<img class="arrow left" src="/static/img/arrow2_e.gif">
<img class="arrow right" src="/static/img/arrow2_e.gif">
</div>
</div>
<div id="screen-gameboy-text">GAME BOY</div>
<div id="screen-nintendo-text">Nintendo</div>
<div id="joystick-pad"></div>
<div id="joystick">.</div>
<div id="control"></div>
<div id="control-b" class="control-button">B</div>
<div id="control-a" class="control-button">A</div>
<div id="start-select-box"></div>
<div id="start-select-button"></div>
<div id="cover-vertical"></div>
<div id="cover-horizontal"></div>
<div id="gloss"></div>
<div id="speaker-holes"></div>
<div id="power"></div>
</div>
<div id="colors">
<div class="color" data-color="red"></div>
<div class="color" data-color="purple"></div>
<div class="color" data-color="green"></div>
<div class="color" data-color="yellow"></div>
<div class="color" data-color="teal"></div>
<div class="color" data-color="transparent"></div><br><br>
<div>Animation powered by bchanx <a href="http://bchanx.com/animated-gameboy-in-css-blog"><i class="fas fa-blog"></i></a> <a href="https://github.com/bchanx/animated-gameboy-in-css"><i class="fab fa-github"></i></a></div><br>
<div>Rocked by <a href="https://github.com/giongto35">giongto35</a> <i class="far fa-handshake"></i> <a href="https://trich.im">trichimtrich</a></div>
</div>
<div style="position:absolute; top:30%; left: 5%;">
<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 (in game) <br />
V is select game <br />
Q is super quit <br />
Room ID: <input type="text" id="roomID"><br>
<!--Fullscreen media for better gaming experience<br /-->
</div><br>
<h3>Log:</h3>
<pre id="div"></pre>
<div>
<i class="fas fa-gamepad"></i> <u><i>Refresh to retry when checking is too long</i></u>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="/static/js/const.js"></script>
<script>
/*
GLOBAL VARS
*/
// Game state
screenState = "loader";
gameIdx = 0;
// Input vars
keyState = {
// controllers
a: false,
b: false,
start: false,
select: false,
// navigators
up: false,
down: false,
left: false,
right: false,
// unofficial
quit: false
}
stateUnchange = true;
unchangePacket = INPUT_STATE_PACKET;
inputTimer = null;
// Connection vars
var pc, inputChannel;
var localSessionDescription = "";
var remoteSessionDescription = "";
var conn;
/*
FUNCTIONS
*/
// miscs
function log(msg) {
if (DEBUG) {
document.getElementById('div').innerHTML += msg + '<br>'
console.log(msg);
}
}
</script>
<script src="/static/js/gameboy_loader.js"></script>
<script src="/static/js/gameboy_controller.js"></script>
<script src="/static/js/{{.Service}}.js"></script>
</body>

BIN
static/img/arrow2_e.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

0
static/index.html Normal file
View file

View file

@ -109,9 +109,7 @@ window.startGame = () => {
// webrtc
pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
// input channel
let r = Math.random().toString(36).substring(7)
console.log("data chan name", r)
inputChannel = pc.createDataChannel(r)
inputChannel = pc.createDataChannel('foo')
inputChannel.onclose = () => {
log('inputChannel has closed');
}
@ -173,6 +171,10 @@ var localSessionDescription = "";
var remoteSessionDescription = "";
var conn;
// Input handler
keyState = {
// controllers

100
static/js/const.js Normal file
View file

@ -0,0 +1,100 @@
// miscs
DEBUG = true;
// list game
GAME_LIST = [
{
name: "Contra",
nes: "Contra.nes",
art: "Contra (USA).png"
},
{
name: "Kirby's Adventure",
nes: "Kirby's Adventure.nes",
art: "Kirby's Adventure (Canada).png"
},
{
name: "Mega Man 2",
nes: "Mega Man 2.nes",
art: "Mega Man 2 (USA).png"
},
{
name: "Metal Gear",
nes: "Metal Gear.nes",
art: "Metal Gear (USA).png"
},
{
name: "Mortal Kombat 4",
nes: "Mortal Kombat 4.nes",
art: "mortal-kombat-x-box-art-revealed-as-pre-orders-ope_5pxd.jpg"
},
{
name: "Super Mario Bros 2",
nes: "Super Mario Bros 2.nes",
art: "Super Mario Bros. 2 (USA).png"
},
{
name: "Super Mario Bros 3",
nes: "Super Mario Bros 3.nes",
art: "Super Mario Bros. 3 (USA).png"
},
{
name: "Super Mario Bros",
nes: "Super Mario Bros.nes",
art: "Super Mario Bros. (World).png"
},
{
name: "TMNT 3",
nes: "Teenage Mutant Ninja Turtles 3.nes",
art: "Teenage Mutant Ninja Turtles III - The Manhattan Project (USA).png"
},
{
name: "Zelda",
nes: "zelda.nes",
art: "Zelda II - The Adventure of Link (Europe) (Rev A).png"
}
];
// Keyboard stuffs
KEY_MAP = {
37: "left",
38: "up",
39: "right",
40: "down",
90: "a", // z
88: "b", // x
67: "start", // c
86: "select", // v
81: "quit" // q
}
/*
const (
ButtonA = iota
ButtonB
ButtonSelect
ButtonStart
ButtonUp
ButtonDown
ButtonLeft
ButtonRight
)
*/
KEY_BIT = ["a", "b", "select", "start", "up", "down", "left", "right"];
INPUT_FPS = 100;
INPUT_STATE_PACKET = 5;

View file

@ -0,0 +1,135 @@
// menu screen
function showMenuScreen() {
log("Clean up connection / frame");
// clean up before / after menu
try {
inputChannel.close();
} catch (err) {
log(`> [Warning] input channel: ${err}`);
}
try {
pc.close();
} catch (err) {
log(`> [Warning] peer connection: ${err}`);
}
try {
conn.close();
} catch (err) {
log(`> [Warning] Websocket connection: ${err}`);
}
$("#loading-screen").hide();
$("#menu-screen").hide();
// show
$("#loading-screen").show().delay(1000).fadeOut(400, () => {
log("Loading menu screen");
$("#menu-screen").fadeIn(400, () => {
chooseGame(7);
screenState = "menu";
});
});
}
// game menu
function chooseGame(idx) {
if (idx < 0 || idx == gameIdx || idx >= GAME_LIST.length) return false;
$("#menu-screen #box-art").fadeOut(400, function () {
$(this).attr("src", `/static/img/boxarts/${GAME_LIST[idx].art}`);
$(this).fadeIn(400, function () {
$("#menu-screen #title p").html(GAME_LIST[idx].name);
});
});
if (idx == 0) {
$("#menu-screen .left").hide();
} else {
$("#menu-screen .left").show();
}
if (idx == GAME_LIST.length - 1) {
$("#menu-screen .right").hide();
} else {
$("#menu-screen .right").show();
}
gameIdx = idx;
log(`> [Pick] game ${gameIdx + 1}/${GAME_LIST.length} - ${GAME_LIST[gameIdx].name}`);
}
function setState(e, bo) {
if (e.keyCode in KEY_MAP) {
keyState[KEY_MAP[e.keyCode]] = bo;
stateUnchange = false;
unchangePacket = INPUT_STATE_PACKET;
}
}
document.body.onkeyup = function (e) {
if (screenState === "menu") {
switch (KEY_MAP[e.keyCode]) {
case "left":
chooseGame(gameIdx - 1);
break;
case "right":
chooseGame(gameIdx + 1);
break;
case "select":
startGame();
}
} else if (screenState === "game") {
setState(e, false);
}
// global reset
if (KEY_MAP[e.keyCode] == "quit") {
endInput();
showMenuScreen();
}
}
document.body.onkeydown = function (e) {
if (screenState === "game")
setState(e, true);
};
function sendInput() {
// prepare key
if (stateUnchange || unchangePacket > 0) {
st = "";
KEY_BIT.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 (inputTimer == null) {
inputTimer = setInterval(sendInput, 1000 / INPUT_FPS)
}
}
function endInput() {
clearInterval(inputTimer);
inputTimer = null;
}

View file

@ -0,0 +1,35 @@
// A tiny bit of javascript code for color selection
window.onload = function() {
var colors = ['red', 'purple', 'green', 'yellow', 'teal', 'transparent'];
var last = null;
Array.prototype.slice.call(document.querySelectorAll('.color')).forEach(function(el) {
el.addEventListener('click', function() {
log("Starting gameboy");
screenState = "loader";
if (last) {
last.classList.remove('active');
}
var color = el.getAttribute('data-color');
var gameboy = document.querySelector('#gameboy');
gameboy.style.opacity = 0;
gameboy.classList.remove(gameboy.classList[0]);
var clone = gameboy.cloneNode(true);
gameboy.remove();
clone.classList.add(color);
clone.style.opacity = 1;
var colors = document.querySelector('#colors');
colors.parentNode.insertBefore(clone, colors);
el.classList.add('active');
last = el;
});
});
document.querySelector('.color[data-color="green"]').dispatchEvent(new MouseEvent('click', {
'view': window,
'bubbles': true
}));
$("#screen-gameboy-text").on("webkitAnimationEnd", showMenuScreen);
$("#screen-gameboy-text").on("animationend", showMenuScreen);
}

94
static/js/http.js Normal file
View file

@ -0,0 +1,94 @@
// 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];
// 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;
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();
}
}
}

114
static/js/ws.js Normal file
View file

@ -0,0 +1,114 @@
// web socket
function startGame() {
log("Starting game screen");
// clear
endInput();
document.getElementById('div').innerHTML = "";
$("#loading-screen").show();
$("#menu-screen").fadeOut();
// end clear
conn = new WebSocket(`ws://${location.host}/ws`);
conn.onopen = () => {
log("WebSocket is opened. Send ping");
roomID = document.getElementById('roomID').value
conn.send(JSON.stringify({"id": "ping", "data": GAME_LIST[gameIdx].nes, "room_id": roomID}));
}
conn.onerror = error => {
log(`Websocket error: ${error}`);
}
conn.onclose = () => {
log("Websocket closed");
}
conn.onmessage = e => {
d = JSON.parse(e.data);
switch (d["id"]) {
case "pong":
log("Recv pong. Start webrtc");
startWebRTC();
break;
case "sdp":
log("Got remote sdp");
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
break;
case "start":
log("Got start");
document.getElementById('roomID').value = d["room_id"]
break;
}
}
// webrtc
pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
// input channel
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.oniceconnectionstatechange = e => {
log(`iceConnectionState: ${pc.iceConnectionState}`);
if (pc.iceConnectionState === "connected") {
conn.send(JSON.stringify({"id": "start", "data": ""}));
startInput();
screenState = "game";
}
else if (pc.iceConnectionState === "disconnected") {
endInput();
}
}
// stream channel
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;
}
// candidate packet from STUN
pc.onicecandidate = event => {
if (event.candidate === null) {
} else {
console.log(JSON.stringify(event.candidate));
// conn.send(JSON.stringify({"id": "candidate", "data": JSON.stringify(event.candidate)}));
}
}
function startWebRTC() {
// receiver only tracks
pc.addTransceiver('video', {'direction': 'recvonly'});
// create SDP
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: false}).then(d => {
pc.setLocalDescription(d, () => {
// send to ws
session = btoa(JSON.stringify(pc.localDescription));
localSessionDescription = session;
log("Send SDP to remote peer");
conn.send(JSON.stringify({"id": "sdp", "data": session}));
});
}).catch(log);
}
}