super cool frontend and new structure

This commit is contained in:
trichimtrich 2019-04-07 10:09:57 +08:00
parent 1e14a49822
commit f9e0fc0bd5
29 changed files with 7875 additions and 258 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -17,8 +17,6 @@ textarea {
<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="play" onclick="window.startGame()">Play</button>
@ -45,242 +43,5 @@ textarea {
<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);
}
}
// web socket
window.startGame = () => {
// clear
endInput();
document.getElementById('div').innerHTML = "";
aa = document.getElementsByTagName("video");
for (i = 0; i < aa.length ; ++i) {
aa[i].remove();
}
// end clear
conn = new WebSocket(`ws://${location.host}/ws`);
conn.onopen = () => {
log("WebSocket is opened. Send ping");
conn.send(JSON.stringify({"id": "ping", "data": gameOp.value}));
}
conn.onerror = error => {
log(`Websocket error: ${error}`);
}
conn.onclose = () => {
log("Websocket closed");
// pc.close();
}
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;
}
}
// 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();
}
else if (pc.iceConnectionState === "disconnected") {
// else { // not sure about this =]
endInput();
}
}
// stream channel
pc.ontrack = function (event) {
var el = document.createElement(event.track.kind);
// console.log(event.streams);
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)
}
// 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)}));
}
}
}
// webrtc
var pc, inputChannel;
var localSessionDescription = "";
var remoteSessionDescription = "";
var conn;
// Input handler
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);
};
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;
}
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);
}
</script>
<script src="/static/ws.js"></script>
</html>

51
main.go
View file

@ -2,6 +2,7 @@ package main
import (
"os"
"html/template"
pionRTC "github.com/pion/webrtc"
@ -15,16 +16,21 @@ import (
"github.com/giongto35/cloud-game/util"
"github.com/giongto35/cloud-game/webrtc"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"encoding/json"
)
// 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{}
@ -33,40 +39,53 @@ type WSPacket struct {
Data string `json:"data"`
}
func init() {
type SessionPacket struct {
Game string `json:"game"`
SDP string `json:"sdp"`
}
func startGame(path string, imageChannel chan *image.RGBA, inputChannel chan int, webRTC *webrtc.WebRTC) {
ui.Run([]string{path}, imageChannel, inputChannel, webRTC)
}
func main() {
fmt.Printf("Usage: %s [ws]\n", os.Args[0])
fmt.Println("http://localhost:8000")
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)
}
func ws(w http.ResponseWriter, r *http.Request) {
@ -153,10 +172,6 @@ func ws(w http.ResponseWriter, r *http.Request) {
}
}
type SessionPacket struct {
Game string `json:"game"`
SDP string `json:"sdp"`
}
func postSession(w http.ResponseWriter, r *http.Request) {
bs, err := ioutil.ReadAll(r.Body)

6922
static/css/gameboy.css Normal file

File diff suppressed because it is too large Load diff

Binary file not shown.

160
static/gameboy.html Normal file
View file

@ -0,0 +1,160 @@
<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 />
<!--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

286
static/index_ws.html Normal file
View file

@ -0,0 +1,286 @@
<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="play" onclick="window.startGame()">Play</button>
<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);
}
}
// web socket
window.startGame = () => {
// clear
endInput();
document.getElementById('div').innerHTML = "";
aa = document.getElementsByTagName("video");
for (i = 0; i < aa.length ; ++i) {
aa[i].remove();
}
// end clear
conn = new WebSocket(`ws://${location.host}/ws`);
conn.onopen = () => {
log("WebSocket is opened. Send ping");
conn.send(JSON.stringify({"id": "ping", "data": gameOp.value}));
}
conn.onerror = error => {
log(`Websocket error: ${error}`);
}
conn.onclose = () => {
log("Websocket closed");
// pc.close();
}
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;
}
}
// 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();
}
else if (pc.iceConnectionState === "disconnected") {
// else { // not sure about this =]
endInput();
}
}
// stream channel
pc.ontrack = function (event) {
var el = document.createElement(event.track.kind);
// console.log(event.streams);
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)
}
// 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)}));
}
}
}
// webrtc
var pc, inputChannel;
var localSessionDescription = "";
var remoteSessionDescription = "";
var conn;
// Input handler
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);
};
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;
}
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);
}
</script>
</html>

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();
}
}
}

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

@ -0,0 +1,109 @@
// 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");
conn.send(JSON.stringify({"id": "ping", "data": GAME_LIST[gameIdx].nes}));
}
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;
}
}
// 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);
}
}