mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-07-25 11:03:56 +00:00
Initial new frontend rewrite (#129)
* Initial new frontend rewrite * Refactor some module deps * Fix options ref in the ajax module * Refactor * Add controller state * Refactor
This commit is contained in:
parent
1adcfb55cb
commit
3d1f1ee587
27 changed files with 1591 additions and 1496 deletions
4
web/css/main.css
vendored
4
web/css/main.css
vendored
|
|
@ -389,6 +389,10 @@ body {
|
|||
background-color: #333 !important;
|
||||
}
|
||||
|
||||
#bottom-screen {
|
||||
/* popups under the screen fix */
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
#game-screen {
|
||||
overflow: hidden;
|
||||
|
|
|
|||
28
web/game.html
vendored
28
web/game.html
vendored
|
|
@ -85,21 +85,25 @@
|
|||
STUNTURN = {{.STUNTURN}};
|
||||
</script>
|
||||
|
||||
<script src="/static/js/jquery-3.3.1.min.js?3"></script>
|
||||
<script src="/static/js/utils.js?3"></script>
|
||||
<script src="/static/js/lib/jquery-3.4.1.min.js"></script>
|
||||
|
||||
<!-- https://rawgit.com/Rillke/opus.js-sample/master/index.xhtml -->
|
||||
<script src="/static/js/libopus.js?3"></script>
|
||||
<script src="/static/js/opus.js?3"></script>
|
||||
<script src="/static/js/log.js?v=1"></script>
|
||||
<script src="/static/js/env.js?v=1"></script>
|
||||
<script src="/static/js/event/event.js?v=1"></script>
|
||||
<script src="/static/js/input/keys.js?v=1"></script>
|
||||
<script src="/static/js/input/input.js?v=1"></script>
|
||||
<script src="/static/js/gameList.js?v=1"></script>
|
||||
<script src="/static/js/room.js?v=1"></script>
|
||||
<script src="/static/js/controller.js?v=1"></script>
|
||||
<script src="/static/js/input/keyboard.js?v=1"></script>
|
||||
<script src="/static/js/input/touch.js?v=1"></script>
|
||||
<script src="/static/js/input/joystick.js?v=1"></script>
|
||||
<script src="/static/js/network/ajax.js?v=1"></script>
|
||||
<script src="/static/js/network/socket.js?v=1"></script>
|
||||
<script src="/static/js/network/rtcp.js?v=1"></script>
|
||||
|
||||
<script src="/static/js/global.js?3"></script>
|
||||
<script src="/static/js/controller.js?3"></script>
|
||||
<script src="/static/js/gesture_keyboard.js?3"></script>
|
||||
<script src="/static/js/gesture_touch.js?3"></script>
|
||||
<script src="/static/js/gesture_joystick.js?3"></script>
|
||||
<script src="/static/js/ws.js?6"></script>
|
||||
<script src="/static/js/init.js?v=1"></script>
|
||||
|
||||
<script src="/static/js/init.js?2"></script>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-145078282-1"></script>
|
||||
<script>
|
||||
|
|
|
|||
545
web/js/controller.js
vendored
545
web/js/controller.js
vendored
|
|
@ -1,262 +1,327 @@
|
|||
/*
|
||||
Menu Controller
|
||||
*/
|
||||
/**
|
||||
* App controller module.
|
||||
* @version 1
|
||||
*/
|
||||
/* const controller = */
|
||||
(() => {
|
||||
// current app state
|
||||
let state;
|
||||
|
||||
function showHelpScreen() {
|
||||
// show btn-save, btn-load
|
||||
if (screenState === "menu") {
|
||||
$("#btn-save").show();
|
||||
$("#btn-load").show();
|
||||
// flags
|
||||
// first user interaction
|
||||
// used for mute/unmute
|
||||
let interacted = false;
|
||||
|
||||
$("#menu-screen").hide();
|
||||
} else {
|
||||
$("#game-screen").hide();
|
||||
}
|
||||
|
||||
// show help overlay
|
||||
$("#help-overlay").show();
|
||||
}
|
||||
|
||||
function hideHelpScreen() {
|
||||
//
|
||||
if (screenState === "menu") {
|
||||
$("#btn-save").hide();
|
||||
$("#btn-load").hide();
|
||||
|
||||
$("#menu-screen").show();
|
||||
} else {
|
||||
$("#game-screen").show();
|
||||
}
|
||||
|
||||
// show help overlay
|
||||
$("#help-overlay").hide();
|
||||
}
|
||||
|
||||
function reloadGameMenu() {
|
||||
log("Load game menu");
|
||||
|
||||
// sort gameList first
|
||||
gameList.sort(function (a, b) {
|
||||
return a > b ? 1 : -1;
|
||||
// UI elements
|
||||
// use $element[0] for DOM element
|
||||
const gameScreen = $('#game-screen');
|
||||
const menuScreen = $('#menu-screen');
|
||||
const helpOverlay = $('#help-overlay');
|
||||
const popupBox = $('#noti-box');
|
||||
// keymap
|
||||
const keyButtons = {};
|
||||
Object.keys(KEY).forEach(button => {
|
||||
keyButtons[KEY[button]] = $(`#btn-${KEY[button]}`);
|
||||
});
|
||||
|
||||
// generate html
|
||||
var listbox = $("#menu-container");
|
||||
listbox.html('');
|
||||
gameList.forEach(function (game) {
|
||||
listbox.append(`<div class="menu-item unselectable" unselectable="on"><div><span>${game}</span></div></div>`);
|
||||
});
|
||||
}
|
||||
const setState = (newState) => {
|
||||
log.debug(`[control] [s] ${state ? state.name : '???'} -> ${newState.name}`);
|
||||
state = newState;
|
||||
};
|
||||
|
||||
function showMenuScreen() {
|
||||
// clear scenes
|
||||
$("#game-screen").hide();
|
||||
$("#menu-screen").hide();
|
||||
$("#btn-save").hide();
|
||||
$("#btn-load").hide();
|
||||
$("#btn-join").html("play");
|
||||
const onGameRoomAvailable = () => {
|
||||
keyButtons[KEY.JOIN].html('share');
|
||||
popup('Now you can share you game!');
|
||||
};
|
||||
|
||||
// show menu scene
|
||||
$("#game-screen").show().delay(DEBUG ? 0 : 0).fadeOut(DEBUG ? 0 : 0, function () {
|
||||
log("Loading menu screen");
|
||||
$("#menu-screen").fadeIn(DEBUG ? 0 : 0, function () {
|
||||
pickGame(gameIdx);
|
||||
screenState = "menu";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function pickGame(idx) {
|
||||
// check boundaries
|
||||
// cycle
|
||||
if (idx < 0) idx = gameList.length - 1;
|
||||
if (idx >= gameList.length) idx = 0;
|
||||
|
||||
// transition menu box
|
||||
|
||||
var listbox = $("#menu-container");
|
||||
listbox.css("transition", "top 0.2s");
|
||||
listbox.css("-moz-transition", "top 0.2s");
|
||||
listbox.css("-webkit-transition", "top 0.2s");
|
||||
|
||||
menuTop = MENU_TOP_POSITION - idx * 36;
|
||||
listbox.css("top", `${menuTop}px`);
|
||||
|
||||
// overflow marquee
|
||||
$(".menu-item .pick").removeClass("pick");
|
||||
$(`.menu-item:eq(${idx}) span`).addClass("pick");
|
||||
|
||||
gameIdx = idx;
|
||||
log(`> [Pick] game ${gameIdx + 1}/${gameList.length} - ${gameList[gameIdx]}`);
|
||||
}
|
||||
|
||||
|
||||
function startGamePickerTimer(direction) {
|
||||
if (gamePickerTimer === null) {
|
||||
pickGame(gameIdx + (direction === "up" ? -1 : 1));
|
||||
|
||||
log("Start game picker timer");
|
||||
// velocity?
|
||||
gamePickerTimer = setInterval(function () {
|
||||
pickGame(gameIdx + (direction === "up" ? -1 : 1));
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
function stopGamePickerTimer() {
|
||||
if (gamePickerTimer !== null) {
|
||||
log("Stop game picker timer");
|
||||
clearInterval(gamePickerTimer);
|
||||
gamePickerTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Game controller
|
||||
*/
|
||||
|
||||
function sendKeyState() {
|
||||
// check if state is changed
|
||||
if (unchangePacket > 0) {
|
||||
// pack keystate
|
||||
var bits = "";
|
||||
KEY_BIT.slice().reverse().forEach(elem => {
|
||||
bits += keyState[elem] ? 1 : 0;
|
||||
});
|
||||
var data = parseInt(bits, 2);
|
||||
|
||||
var arrBuf = new Uint8Array(2);
|
||||
arrBuf[0] = data & ((1 << 8) - 1);
|
||||
arrBuf[1] = data >> 8;
|
||||
inputChannel.send(arrBuf);
|
||||
|
||||
unchangePacket--;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function startGameInputTimer() {
|
||||
if (gameInputTimer === null) {
|
||||
log("Start game input timer");
|
||||
gameInputTimer = setInterval(sendKeyState, 1000 / INPUT_FPS)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function stopGameInputTimer() {
|
||||
if (gameInputTimer !== null) {
|
||||
log("Stop game input timer");
|
||||
clearInterval(gameInputTimer);
|
||||
gameInputTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function setKeyState(name, state) {
|
||||
if (name in keyState) {
|
||||
keyState[name] = state;
|
||||
unchangePacket = INPUT_STATE_PACKET;
|
||||
}
|
||||
}
|
||||
|
||||
function doButtonDown(name) {
|
||||
$(`#btn-${name}`).addClass("pressed");
|
||||
|
||||
if (screenState === "menu") {
|
||||
if (name === "up" || name === "down") {
|
||||
startGamePickerTimer(name);
|
||||
const onConnectionReady = () => {
|
||||
// start a game right away or show the menu
|
||||
if (room.getId()) {
|
||||
startGame();
|
||||
} else {
|
||||
state.menuReady();
|
||||
}
|
||||
} else if (screenState === "game") {
|
||||
setKeyState(name, true);
|
||||
}
|
||||
};
|
||||
|
||||
if (name === "help") {
|
||||
showHelpScreen();
|
||||
}
|
||||
}
|
||||
const onLatencyCheckRequest = (data) => {
|
||||
popup('Ping check...');
|
||||
const timeoutMs = 2000;
|
||||
const maxTimeoutMs = timeoutMs > ajax.defaultTimeoutMs() ? timeoutMs : ajax.defaultTimeoutMs();
|
||||
|
||||
Promise.all((data.addresses || []).map(address => {
|
||||
let beforeTime = Date.now();
|
||||
return ajax.fetch(`http://${address}:9000/echo?_=${beforeTime}`, {}, timeoutMs)
|
||||
.then(() => ({[address]: Date.now() - beforeTime}), () => ({[address]: maxTimeoutMs}));
|
||||
})).then(results => {
|
||||
// const latencies = Object.assign({}, ...results);
|
||||
const latencies = {};
|
||||
results.map(latency => Object.keys(latency).forEach(address => latencies[address] = latency[address]));
|
||||
log.info('[ping] <->', latencies);
|
||||
socket.latency(latencies, data.packetId);
|
||||
});
|
||||
};
|
||||
|
||||
function doButtonUp(name) {
|
||||
$(`#btn-${name}`).removeClass("pressed");
|
||||
const helpScreen = {
|
||||
// don't call $ if holding the button
|
||||
shown: false,
|
||||
// undo the state when release the button
|
||||
prevState: null,
|
||||
// use function () if you need "this"
|
||||
show: function (show) {
|
||||
if (this.shown === show) return;
|
||||
|
||||
if (screenState === "menu") {
|
||||
switch (name) {
|
||||
case "up":
|
||||
case "down":
|
||||
stopGamePickerTimer();
|
||||
break;
|
||||
|
||||
case "join":
|
||||
case "a":
|
||||
case "b":
|
||||
case "x":
|
||||
case "y":
|
||||
case "start":
|
||||
case "select":
|
||||
startGame();
|
||||
break;
|
||||
|
||||
case "quit":
|
||||
popup("You are already in menu screen!");
|
||||
break;
|
||||
|
||||
case "load":
|
||||
popup("Lets play to load game!");
|
||||
break;
|
||||
|
||||
case "save":
|
||||
popup("Lets play to save game!");
|
||||
break;
|
||||
|
||||
// hack
|
||||
if (state === app.state.game || this.prevState === app.state.game) {
|
||||
gameScreen.toggle(!show);
|
||||
} else {
|
||||
keyButtons[KEY.SAVE].toggle(show);
|
||||
keyButtons[KEY.LOAD].toggle(show);
|
||||
menuScreen.toggle(!show);
|
||||
}
|
||||
helpOverlay.toggle(show);
|
||||
|
||||
this.shown = show;
|
||||
|
||||
if (show) {
|
||||
this.prevState = state;
|
||||
setState(app.state.help);
|
||||
} else {
|
||||
setState(this.prevState);
|
||||
}
|
||||
}
|
||||
} else if (screenState === "game") {
|
||||
setKeyState(name, false);
|
||||
};
|
||||
|
||||
switch (name) {
|
||||
case "join":
|
||||
copyToClipboard(window.location.href.split('?')[0] + `?id=${encodeURIComponent(roomID)}`)
|
||||
popup("Copy link to clipboard!")
|
||||
break;
|
||||
const showMenuScreen = () => {
|
||||
// clear scenes
|
||||
gameScreen.hide();
|
||||
menuScreen.hide();
|
||||
gameList.hide();
|
||||
keyButtons[KEY.SAVE].hide();
|
||||
keyButtons[KEY.LOAD].hide();
|
||||
keyButtons[KEY.JOIN].html('play');
|
||||
|
||||
case "save":
|
||||
conn.send(JSON.stringify({ "id": "save", "data": "" }));
|
||||
break;
|
||||
// show menu scene
|
||||
gameScreen.show().delay(0).fadeOut(0, () => {
|
||||
log.debug('[control] loading menu screen');
|
||||
menuScreen.fadeIn(0, () => {
|
||||
gameList.show();
|
||||
setState(app.state.menu);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
case "load":
|
||||
conn.send(JSON.stringify({ "id": "load", "data": "" }));
|
||||
break;
|
||||
|
||||
case "full":
|
||||
// Fullscreen
|
||||
screen = document.getElementById("game-screen");
|
||||
const startGame = () => {
|
||||
if (!rtcp.isConnected()) {
|
||||
popup('Game cannot load. Please refresh');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(screen.height, window.innerHeight);
|
||||
if (screen.height === window.innerHeight) {
|
||||
closeFullscreen();
|
||||
} else {
|
||||
openFullscreen(screen);
|
||||
if (!rtcp.isInputReady()) {
|
||||
popup('Game is not ready yet. Please wait');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('[control] starting game screen');
|
||||
|
||||
setState(app.state.game);
|
||||
|
||||
const promise = gameScreen[0].play();
|
||||
if (promise !== undefined) {
|
||||
promise.then(() => log.info('Media can autoplay'))
|
||||
.catch(error => {
|
||||
// Usually error happens when we autoplay unmuted video, browser requires manual play.
|
||||
// We already muted video and use separate audio encoding so it's fine now
|
||||
log.error('Media Failed to autoplay');
|
||||
log.error(error)
|
||||
// TODO: Consider workaround
|
||||
});
|
||||
}
|
||||
|
||||
// TODO get current game from the URL and not from the list?
|
||||
// if we are opening a share link it will send the default game name to the server
|
||||
// currently it's a game with the index 1
|
||||
// on the server this game is ignored and the actual game will be extracted from the share link
|
||||
// so there's no point in doing this and this' really confusing
|
||||
socket.startGame(gameList.getCurrentGame(), env.isMobileDevice(), room.getId(), 1);
|
||||
|
||||
// clear menu screen
|
||||
input.poll().disable();
|
||||
menuScreen.hide();
|
||||
gameScreen.show();
|
||||
keyButtons[KEY.SAVE].show();
|
||||
keyButtons[KEY.LOAD].show();
|
||||
// end clear
|
||||
input.poll().enable();
|
||||
};
|
||||
|
||||
// !to add debounce
|
||||
const popup = (msg) => {
|
||||
popupBox.html(msg);
|
||||
popupBox.fadeIn().delay(0).fadeOut();
|
||||
};
|
||||
|
||||
const onKeyPress = (data) => {
|
||||
keyButtons[data.key].addClass('pressed');
|
||||
|
||||
if (KEY.HELP === data.key) helpScreen.show(true);
|
||||
|
||||
state.keyPress(data.key);
|
||||
};
|
||||
|
||||
const onKeyRelease = (data) => {
|
||||
keyButtons[data.key].removeClass('pressed');
|
||||
|
||||
if (KEY.HELP === data.key) helpScreen.show(false);
|
||||
|
||||
// maybe move it somewhere
|
||||
if (!interacted) {
|
||||
// unmute when there is user interaction
|
||||
gameScreen[0].muted = false;
|
||||
interacted = true;
|
||||
}
|
||||
|
||||
state.keyRelease(data.key);
|
||||
};
|
||||
|
||||
const app = {
|
||||
state: {
|
||||
eden: {
|
||||
name: 'eden',
|
||||
keyPress: () => {
|
||||
},
|
||||
keyRelease: () => {
|
||||
},
|
||||
menuReady: () => {
|
||||
showMenuScreen()
|
||||
}
|
||||
break;
|
||||
},
|
||||
|
||||
case "quit":
|
||||
location.reload()
|
||||
stopGameInputTimer();
|
||||
showMenuScreen();
|
||||
|
||||
// TODO: Stop game
|
||||
conn.send(JSON.stringify({ "id": "quit", "data": "", "room_id": roomID }));
|
||||
help: {
|
||||
name: 'help',
|
||||
keyPress: () => {
|
||||
},
|
||||
keyRelease: () => {
|
||||
},
|
||||
menuReady: () => {
|
||||
// show silently
|
||||
gameScreen.hide();
|
||||
menuScreen.hide();
|
||||
gameList.hide();
|
||||
keyButtons[KEY.JOIN].html('play');
|
||||
|
||||
roomID = ""
|
||||
$("#room-txt").val(roomID);
|
||||
popup("Quit!");
|
||||
break;
|
||||
gameList.show();
|
||||
|
||||
helpScreen.prevState = app.state.menu;
|
||||
}
|
||||
},
|
||||
|
||||
menu: {
|
||||
name: 'menu',
|
||||
keyPress: (key) => {
|
||||
switch (key) {
|
||||
case KEY.UP:
|
||||
case KEY.DOWN:
|
||||
gameList.startGamePickerTimer(key === KEY.UP);
|
||||
break;
|
||||
}
|
||||
},
|
||||
keyRelease: (key) => {
|
||||
switch (key) {
|
||||
case KEY.UP:
|
||||
case KEY.DOWN:
|
||||
gameList.stopGamePickerTimer();
|
||||
break;
|
||||
case KEY.JOIN:
|
||||
case KEY.A:
|
||||
case KEY.B:
|
||||
case KEY.X:
|
||||
case KEY.Y:
|
||||
case KEY.START:
|
||||
case KEY.SELECT:
|
||||
startGame();
|
||||
break;
|
||||
case KEY.QUIT:
|
||||
popup('You are already in menu screen!');
|
||||
break;
|
||||
case KEY.LOAD:
|
||||
popup('Lets play to load game!');
|
||||
break;
|
||||
case KEY.SAVE:
|
||||
popup('Lets play to save game!');
|
||||
break;
|
||||
}
|
||||
},
|
||||
menuReady: () => {
|
||||
}
|
||||
},
|
||||
|
||||
game: {
|
||||
name: 'game',
|
||||
keyPress: (key) => {
|
||||
input.setKeyState(key, true);
|
||||
},
|
||||
keyRelease: function (key) {
|
||||
input.setKeyState(key, false);
|
||||
|
||||
switch (key) {
|
||||
// nani? why join / copy switch, it's confusing
|
||||
case KEY.JOIN:
|
||||
room.copyToClipboard();
|
||||
popup('Copy link to clipboard!');
|
||||
break;
|
||||
case KEY.SAVE:
|
||||
socket.saveGame();
|
||||
break;
|
||||
case KEY.LOAD:
|
||||
socket.loadGame();
|
||||
break;
|
||||
case KEY.FULL:
|
||||
env.display().toggleFullscreen(gameScreen.height() !== window.innerHeight, gameScreen[0]);
|
||||
break;
|
||||
case KEY.QUIT:
|
||||
input.poll().disable();
|
||||
|
||||
// TODO: Stop game
|
||||
socket.quitGame(room.getId());
|
||||
room.reset();
|
||||
|
||||
popup('Quit!');
|
||||
|
||||
location.reload();
|
||||
break;
|
||||
}
|
||||
|
||||
},
|
||||
menuReady: () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (name === "help") {
|
||||
hideHelpScreen();
|
||||
}
|
||||
}
|
||||
// subscriptions
|
||||
event.sub(GAME_ROOM_AVAILABLE, onGameRoomAvailable, 2);
|
||||
event.sub(GAME_SAVED, () => popup('Saved'));
|
||||
event.sub(GAME_LOADED, () => popup('Loaded'));
|
||||
event.sub(MEDIA_STREAM_INITIALIZED, (data) => {
|
||||
rtcp.start(data.stunturn);
|
||||
gameList.set(data.games);
|
||||
});
|
||||
event.sub(MEDIA_STREAM_SDP_AVAILABLE, (data) => rtcp.setRemoteDescription(data.sdp, gameScreen[0]));
|
||||
event.sub(MEDIA_STREAM_READY, () => rtcp.start());
|
||||
event.sub(CONNECTION_READY, onConnectionReady);
|
||||
event.sub(CONNECTION_CLOSED, () => input.poll().disable());
|
||||
event.sub(LATENCY_CHECK_REQUESTED, onLatencyCheckRequest);
|
||||
event.sub(GAMEPAD_CONNECTED, () => popup('Gamepad connected'));
|
||||
event.sub(GAMEPAD_DISCONNECTED, () => popup('Gamepad disconnected'));
|
||||
// touch stuff
|
||||
event.sub(MENU_HANDLER_ATTACHED, (data) => {
|
||||
menuScreen.on(data.event, data.handler);
|
||||
});
|
||||
event.sub(KEY_PRESSED, onKeyPress);
|
||||
event.sub(KEY_RELEASED, onKeyRelease);
|
||||
event.sub(KEY_STATE_UPDATED, data => rtcp.input(data));
|
||||
|
||||
// initial app state
|
||||
setState(app.state.eden);
|
||||
|
||||
})($, document, event, env, gameList, input, KEY, log, room);
|
||||
|
|
|
|||
137
web/js/env.js
vendored
Normal file
137
web/js/env.js
vendored
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
const env = (() => {
|
||||
const win = $(window);
|
||||
const doc = $(document);
|
||||
|
||||
// Screen state
|
||||
let isLayoutSwitched = false;
|
||||
|
||||
// Window rerender / rotate screen if needed
|
||||
const fixScreenLayout = () => {
|
||||
let targetWidth = doc.width() * 0.9;
|
||||
let targetHeight = doc.height() * 0.9;
|
||||
|
||||
// mobile == full screen
|
||||
if (env.getOs() === 'android') {
|
||||
targetWidth = doc.width();
|
||||
targetHeight = doc.height();
|
||||
}
|
||||
|
||||
// Should have maximum box for desktop?
|
||||
// targetWidth = 800; targetHeight = 600; // test on desktop
|
||||
|
||||
fixElementLayout($('#gamebody'), targetWidth, targetHeight);
|
||||
|
||||
const elem = $('#ribbon');
|
||||
let st = '';
|
||||
if (isLayoutSwitched) {
|
||||
st = 'rotate(90deg)';
|
||||
elem.css('bottom', 0);
|
||||
elem.css('top', '');
|
||||
} else {
|
||||
elem.css('bottom', '');
|
||||
elem.css('top', 0);
|
||||
}
|
||||
elem.css('transform', st);
|
||||
elem.css('-webkit-transform', st);
|
||||
elem.css('-moz-transform', st);
|
||||
};
|
||||
|
||||
const fixElementLayout = (elem, targetWidth, targetHeight) => {
|
||||
let st = 'translate(-50%, -50%) ';
|
||||
|
||||
// rotate portrait layout
|
||||
if (isPortrait()) {
|
||||
st += `rotate(90deg) `;
|
||||
let tmp = targetHeight;
|
||||
targetHeight = targetWidth;
|
||||
targetWidth = tmp;
|
||||
isLayoutSwitched = true;
|
||||
} else {
|
||||
isLayoutSwitched = false;
|
||||
}
|
||||
|
||||
// scale, fit to target size
|
||||
st += `scale(${Math.min(targetWidth / elem.width(), targetHeight / elem.height())}) `;
|
||||
|
||||
elem.css('transform', st);
|
||||
elem.css('-webkit-transform', st);
|
||||
elem.css('-moz-transform', st);
|
||||
};
|
||||
|
||||
const getOS = () => {
|
||||
// linux? ios?
|
||||
let OSName = 'unknown';
|
||||
if (navigator.appVersion.indexOf('Win') !== -1) OSName = 'win';
|
||||
else if (navigator.appVersion.indexOf('Mac') !== -1) OSName = 'mac';
|
||||
else if (navigator.userAgent.indexOf('Linux') !== -1) OSName = 'linux';
|
||||
else if (navigator.userAgent.indexOf('Android') !== -1) OSName = 'android';
|
||||
return OSName;
|
||||
};
|
||||
|
||||
const getBrowser = () => {
|
||||
let browserName = 'unknown';
|
||||
if (navigator.userAgent.indexOf('Firefox') !== -1) browserName = 'firefox';
|
||||
if (navigator.userAgent.indexOf('Chrome') !== -1) browserName = 'chrome';
|
||||
if (navigator.userAgent.indexOf('Edge') !== -1) browserName = 'edge';
|
||||
if (navigator.userAgent.indexOf('Version/') !== -1) browserName = 'safari';
|
||||
if (navigator.userAgent.indexOf('UCBrowser') !== -1) browserName = 'uc';
|
||||
return browserName;
|
||||
};
|
||||
|
||||
// !to use more sophisticated approach / lib
|
||||
const isPortrait = () => {
|
||||
// ios / mobile app
|
||||
switch (window.orientation) {
|
||||
case 0:
|
||||
case 180:
|
||||
return true;
|
||||
}
|
||||
|
||||
// desktop
|
||||
const orientation = screen.msOrientation || screen.mozOrientation || (screen.orientation || {}).type;
|
||||
return orientation === 'portrait-primary';
|
||||
};
|
||||
|
||||
const toggleFullscreen = (enable, element) => {
|
||||
const el = enable ? element : document;
|
||||
|
||||
if (enable) {
|
||||
if (el.requestFullscreen) {
|
||||
el.requestFullscreen();
|
||||
} else if (el.mozRequestFullScreen) { /* Firefox */
|
||||
el.mozRequestFullScreen();
|
||||
} else if (el.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
|
||||
el.webkitRequestFullscreen();
|
||||
} else if (el.msRequestFullscreen) { /* IE/Edge */
|
||||
el.msRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (el.exitFullscreen) {
|
||||
el.exitFullscreen();
|
||||
} else if (el.mozCancelFullScreen) { /* Firefox */
|
||||
el.mozCancelFullScreen();
|
||||
} else if (el.webkitExitFullscreen) { /* Chrome, Safari and Opera */
|
||||
el.webkitExitFullscreen();
|
||||
} else if (el.msExitFullscreen) { /* IE/Edge */
|
||||
el.msExitFullscreen();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// bindings
|
||||
win.on('resize', fixScreenLayout);
|
||||
win.on('orientationchange', fixScreenLayout);
|
||||
|
||||
return {
|
||||
getOs: getOS,
|
||||
getBrowser: getBrowser,
|
||||
// Check mobile type because different mobile can accept different video encoder.
|
||||
isMobileDevice: () => (typeof window.orientation !== 'undefined') || (navigator.userAgent.indexOf('IEMobile') !== -1),
|
||||
display: () => ({
|
||||
isPortrait: isPortrait,
|
||||
toggleFullscreen: toggleFullscreen,
|
||||
fixScreenLayout: fixScreenLayout,
|
||||
isLayoutSwitched: isLayoutSwitched
|
||||
})
|
||||
}
|
||||
})($, document, log, navigator, screen, window);
|
||||
80
web/js/event/event.js
vendored
Normal file
80
web/js/event/event.js
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Event publishing / subscribe module.
|
||||
* Just a simple observer pattern.
|
||||
* @version 1
|
||||
*/
|
||||
const event = (() => {
|
||||
const topics = {};
|
||||
|
||||
return {
|
||||
/**
|
||||
* Subscribes onto some event.
|
||||
*
|
||||
* @param topic The name of the event.
|
||||
* @param listener A callback function to call during the event.
|
||||
* @param order A number in a queue of event handlers to run callback in ordered manner.
|
||||
* @returns {{unsub: unsub}} The function to remove this subscription.
|
||||
* @example
|
||||
* const sub01 = event.sub('rapture', () => {a}, 1)
|
||||
* ...
|
||||
* sub01.unsub()
|
||||
*/
|
||||
sub: (topic, listener, order) => {
|
||||
if (!topics[topic]) topics[topic] = [];
|
||||
|
||||
// order handling stuff
|
||||
const value = {order: order || 0, listener: listener};
|
||||
topics[topic].push(value);
|
||||
topics[topic].sort((a, b) => a.order - b.order);
|
||||
const index = topics[topic].indexOf(value);
|
||||
|
||||
return {
|
||||
unsub: () => {
|
||||
topics[topic].splice(index, 1);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Publishes some event for handling.
|
||||
*
|
||||
* @param topic The name of the event.
|
||||
* @param data Additional data for the event handling.
|
||||
* Because of compatibility we have to use a dumb obj wrapper {a: a, b: b} for params instead of (topic, ...data).
|
||||
* @example
|
||||
* event.pub('rapture', {time: now()})
|
||||
*/
|
||||
pub: (topic, data) => {
|
||||
if (!topics[topic] || topics[topic].length < 1) return;
|
||||
|
||||
topics[topic].forEach((listener) => {
|
||||
listener.listener(data || {})
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// events
|
||||
const LATENCY_CHECK_REQUESTED = 'latencyCheckRequested';
|
||||
|
||||
const GAME_ROOM_AVAILABLE = 'gameRoomAvailable';
|
||||
const GAME_SAVED = 'gameSaved';
|
||||
const GAME_LOADED = 'gameLoaded';
|
||||
|
||||
const CONNECTION_READY = 'connectionReady';
|
||||
const CONNECTION_CLOSED = 'connectionClosed';
|
||||
|
||||
const MEDIA_STREAM_INITIALIZED = 'mediaStreamInitialized';
|
||||
const MEDIA_STREAM_SDP_AVAILABLE = 'mediaStreamSdpAvailable';
|
||||
const MEDIA_STREAM_READY = 'mediaStreamReady';
|
||||
|
||||
const GAMEPAD_CONNECTED = 'gamepadConnected';
|
||||
const GAMEPAD_DISCONNECTED = 'gamepadDisconnected';
|
||||
|
||||
const MENU_HANDLER_ATTACHED = 'menuHandlerAttached';
|
||||
const MENU_PRESSED = 'menuPressed';
|
||||
const MENU_RELEASED = 'menuReleased';
|
||||
|
||||
const KEY_PRESSED = 'keyPressed';
|
||||
const KEY_RELEASED = 'keyReleased';
|
||||
const KEY_STATE_UPDATED = 'keyStateUpdated';
|
||||
111
web/js/gameList.js
vendored
Normal file
111
web/js/gameList.js
vendored
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/**
|
||||
* Game list module.
|
||||
* @version 1
|
||||
*/
|
||||
const gameList = (() => {
|
||||
// state
|
||||
let games = [];
|
||||
let gameIndex = 1;
|
||||
let gamePickTimer = null;
|
||||
|
||||
// UI
|
||||
const listBox = $('#menu-container');
|
||||
const menuItemChoice = $('#menu-item-choice');
|
||||
|
||||
const MENU_TOP_POSITION = 102;
|
||||
let menuTop = MENU_TOP_POSITION;
|
||||
|
||||
const setGames = (gameList) => {
|
||||
games = gameList.sort((a, b) => a > b ? 1 : -1);
|
||||
};
|
||||
|
||||
const render = () => {
|
||||
log.debug('[games] load game menu');
|
||||
|
||||
listBox.html(games
|
||||
.map(game => `<div class="menu-item unselectable" unselectable="on"><div><span>${game}</span></div></div>`)
|
||||
.join('')
|
||||
);
|
||||
};
|
||||
|
||||
const show = () => {
|
||||
render();
|
||||
menuItemChoice.show();
|
||||
pickGame();
|
||||
};
|
||||
|
||||
const hide = () => {
|
||||
menuItemChoice.hide();
|
||||
};
|
||||
|
||||
const pickGame = (index) => {
|
||||
let idx = undefined !== index ? index : gameIndex;
|
||||
|
||||
// check boundaries
|
||||
// cycle
|
||||
if (idx < 0) idx = games.length - 1;
|
||||
if (idx >= games.length) idx = 0;
|
||||
|
||||
// transition menu box
|
||||
listBox.css('transition', 'top 0.2s');
|
||||
listBox.css('-moz-transition', 'top 0.2s');
|
||||
listBox.css('-webkit-transition', 'top 0.2s');
|
||||
|
||||
menuTop = MENU_TOP_POSITION - idx * 36;
|
||||
listBox.css('top', `${menuTop}px`);
|
||||
|
||||
// overflow marquee
|
||||
$('.menu-item .pick').removeClass('pick');
|
||||
$(`.menu-item:eq(${idx}) span`).addClass('pick');
|
||||
|
||||
gameIndex = idx;
|
||||
};
|
||||
|
||||
const startGamePickerTimer = (upDirection) => {
|
||||
if (gamePickTimer !== null) return;
|
||||
|
||||
log.debug('[games] start game picker timer');
|
||||
const shift = upDirection ? -1 : 1;
|
||||
pickGame(gameIndex + shift);
|
||||
|
||||
// velocity?
|
||||
// keep rolling the game list if the button is pressed
|
||||
gamePickTimer = setInterval(() => {
|
||||
pickGame(gameIndex + shift);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const stopGamePickerTimer = () => {
|
||||
if (gamePickTimer === null) return;
|
||||
|
||||
log.debug('[games] stop game picker timer');
|
||||
clearInterval(gamePickTimer);
|
||||
gamePickTimer = null;
|
||||
};
|
||||
|
||||
const onMenuPressed = (newPosition) => {
|
||||
listBox.css('transition', '');
|
||||
listBox.css('-moz-transition', '');
|
||||
listBox.css('-webkit-transition', '');
|
||||
listBox.css('top', `${menuTop - newPosition}px`);
|
||||
};
|
||||
|
||||
const onMenuReleased = (position) => {
|
||||
menuTop -= position;
|
||||
const index = Math.round((menuTop - MENU_TOP_POSITION) / -36);
|
||||
pickGame(index);
|
||||
};
|
||||
|
||||
event.sub(MENU_PRESSED, onMenuPressed);
|
||||
event.sub(MENU_RELEASED, onMenuReleased);
|
||||
|
||||
return {
|
||||
startGamePickerTimer: startGamePickerTimer,
|
||||
stopGamePickerTimer: stopGamePickerTimer,
|
||||
pickGame: pickGame,
|
||||
show: show,
|
||||
hide: hide,
|
||||
set: setGames,
|
||||
getCurrentGame: () => games[gameIndex]
|
||||
}
|
||||
})($, event, log);
|
||||
141
web/js/gesture_joystick.js
vendored
141
web/js/gesture_joystick.js
vendored
|
|
@ -1,141 +0,0 @@
|
|||
/*
|
||||
Joystick gesture
|
||||
*/
|
||||
|
||||
/*
|
||||
cross == a <--> a
|
||||
circle == b <--> b
|
||||
square == x <--> start
|
||||
triangle == y <--> select
|
||||
share <--> load
|
||||
option <--> save
|
||||
L2 == LT <--> full
|
||||
R2 == RT <--> quit
|
||||
dpad <--> up down left right
|
||||
axis 0, 1 <--> second dpad
|
||||
*/
|
||||
|
||||
/*
|
||||
change full to help (temporary)
|
||||
*/
|
||||
|
||||
let joystickMap;
|
||||
let joystickState;
|
||||
let joystickIdx;
|
||||
let joystickTimer = null;
|
||||
|
||||
|
||||
// check state for each axis -> dpad
|
||||
function checkJoystickAxisState(name, state) {
|
||||
if (joystickState[name] !== state) {
|
||||
joystickState[name] = state;
|
||||
if (state === true) {
|
||||
doButtonDown(name);
|
||||
} else {
|
||||
doButtonUp(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// loop timer for checking joystick state
|
||||
function checkJoystickState() {
|
||||
var gamepad = navigator.getGamepads()[joystickIdx];
|
||||
if (gamepad) {
|
||||
// axis -> dpad
|
||||
var corX = gamepad.axes[0]; // -1 -> 1, left -> right
|
||||
var corY = gamepad.axes[1]; // -1 -> 1, up -> down
|
||||
checkJoystickAxisState("left", corX <= -0.5);
|
||||
checkJoystickAxisState("right", corX >= 0.5);
|
||||
checkJoystickAxisState("up", corY <= -0.5);
|
||||
checkJoystickAxisState("down", corY >= 0.5);
|
||||
|
||||
// normal button map
|
||||
Object.keys(joystickMap).forEach(function (btnIdx) {
|
||||
var isPressed = false;
|
||||
|
||||
if (navigator.webkitGetGamepads) {
|
||||
isPressed = (gamepad.buttons[btnIdx] === 1);
|
||||
} else {
|
||||
isPressed = (gamepad.buttons[btnIdx].value > 0 || gamepad.buttons[btnIdx].pressed === true);
|
||||
}
|
||||
|
||||
if (joystickState[btnIdx] !== isPressed) {
|
||||
joystickState[btnIdx] = isPressed;
|
||||
if (isPressed === true) {
|
||||
doButtonDown(joystickMap[btnIdx]);
|
||||
} else {
|
||||
doButtonUp(joystickMap[btnIdx]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// we only capture the last plugged joystick
|
||||
window.addEventListener("gamepadconnected", function (event) {
|
||||
var gamepad = event.gamepad;
|
||||
log(`Gamepad connected at index ${gamepad.index}: ${gamepad.id}. ${gamepad.buttons.length} buttons, ${gamepad.axes.length} axes.`);
|
||||
|
||||
joystickIdx = gamepad.index;
|
||||
|
||||
// Ref: https://github.com/giongto35/cloud-game/issues/14
|
||||
// get mapping first (default KeyMap2)
|
||||
var os = getOS();
|
||||
var browser = getBrowser();
|
||||
|
||||
if (os === "android") {
|
||||
// default of android is KeyMap1
|
||||
joystickMap = { 2: "a", 0: "b", 3: "start", 4: "select", 10: "load", 11: "save", 8: "help", 9: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
} else {
|
||||
// default of other OS is KeyMap2
|
||||
joystickMap = { 0: "a", 1: "b", 2: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
}
|
||||
|
||||
if (os === "android" && (browser === "firefox" || browser === "uc")) { //KeyMap2
|
||||
joystickMap = { 0: "a", 1: "b", 2: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 12: "up", 13: "down", 14: "left", 15: "right" };
|
||||
}
|
||||
|
||||
if (os === "win" && browser === "firefox") { //KeyMap3
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit" };
|
||||
}
|
||||
|
||||
if (os === "mac" && browser === "safari") { //KeyMap4
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 14: "up", 15: "down", 16: "left", 17: "right" };
|
||||
}
|
||||
|
||||
if (os === "mac" && browser === "firefox") { //KeyMap5
|
||||
joystickMap = { 1: "a", 2: "b", 0: "start", 3: "select", 8: "load", 9: "save", 6: "help", 7: "quit", 14: "up", 15: "down", 16: "left", 17: "right" };
|
||||
}
|
||||
|
||||
|
||||
// reset state
|
||||
joystickState = {
|
||||
left: false,
|
||||
right: false,
|
||||
up: false,
|
||||
down: false,
|
||||
};
|
||||
Object.keys(joystickMap).forEach(function (btnIdx) {
|
||||
joystickState[btnIdx] = false;
|
||||
});
|
||||
|
||||
|
||||
// looper, too intense?
|
||||
if (joystickTimer !== null) {
|
||||
clearInterval(joystickTimer);
|
||||
}
|
||||
|
||||
joystickTimer = setInterval(checkJoystickState, 10); // miliseconds per hit
|
||||
|
||||
});
|
||||
|
||||
|
||||
// disconnected event is triggered
|
||||
window.addEventListener("gamepaddisconnected", (event) => {
|
||||
clearInterval(joystickTimer);
|
||||
log(`Gamepad disconnected at index ${e.gamepad.index}`);
|
||||
});
|
||||
|
||||
|
||||
40
web/js/gesture_keyboard.js
vendored
40
web/js/gesture_keyboard.js
vendored
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
Keyboard gesture
|
||||
*/
|
||||
|
||||
const KEYBOARD_MAP = {
|
||||
37: "left",
|
||||
38: "up",
|
||||
39: "right",
|
||||
40: "down",
|
||||
|
||||
90: "a", // z
|
||||
88: "b", // x
|
||||
67: "x", // c
|
||||
86: "y", // v
|
||||
13: "start", // start
|
||||
16: "select", // select
|
||||
|
||||
// non-game
|
||||
81: "quit", // q
|
||||
83: "save", // s
|
||||
87: "join", // w
|
||||
65: "load", // a
|
||||
70: "full", // f
|
||||
72: "help", // h
|
||||
}
|
||||
|
||||
$("body").on("keyup", function (event) {
|
||||
if (event.keyCode in KEYBOARD_MAP) {
|
||||
doButtonUp(KEYBOARD_MAP[event.keyCode]);
|
||||
// unmute when there is user interaction
|
||||
document.getElementById("game-screen").muted = false;
|
||||
}
|
||||
});
|
||||
|
||||
$("body").on("keydown", function (event) {
|
||||
if (event.keyCode in KEYBOARD_MAP) {
|
||||
doButtonDown(KEYBOARD_MAP[event.keyCode]);
|
||||
}
|
||||
});
|
||||
|
||||
286
web/js/gesture_touch.js
vendored
286
web/js/gesture_touch.js
vendored
|
|
@ -1,286 +0,0 @@
|
|||
/*
|
||||
Touch gesture
|
||||
*/
|
||||
|
||||
// Virtual Gamepad / Joystick
|
||||
// Ref: https://jsfiddle.net/aa0et7tr/5/
|
||||
|
||||
|
||||
/*
|
||||
Left panel - Dpad
|
||||
*/
|
||||
const MAX_DIFF = 20; // radius of circle boundary
|
||||
|
||||
// vpad state, use for mouse button down
|
||||
let vpadState = {
|
||||
up: false,
|
||||
down: false,
|
||||
left: false,
|
||||
right: false,
|
||||
};
|
||||
$(".btn, .btn-big").each(function () {
|
||||
vpadState[$(this).attr("value")] = false;
|
||||
});
|
||||
|
||||
let vpadTouchIdx = null;
|
||||
let vpadTouchDrag = null;
|
||||
let vpadHolder = $("#circle-pad-holder");
|
||||
let vpadCircle = $("#circle-pad");
|
||||
|
||||
|
||||
function resetVpadState() {
|
||||
// trigger up event?
|
||||
checkVpadState("up", false);
|
||||
checkVpadState("down", false);
|
||||
checkVpadState("left", false);
|
||||
checkVpadState("right", false);
|
||||
|
||||
vpadTouchDrag = null;
|
||||
vpadTouchIdx = null;
|
||||
$(".dpad").removeClass("pressed");
|
||||
}
|
||||
|
||||
|
||||
function checkVpadState(axis, state) {
|
||||
if (state !== vpadState[axis]) {
|
||||
vpadState[axis] = state;
|
||||
|
||||
if (state) {
|
||||
doButtonDown(axis);
|
||||
} else {
|
||||
doButtonUp(axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleVpadJoystickDown(event) {
|
||||
vpadCircle.css("transition", "0s");
|
||||
vpadCircle.css("-moz-transition", "0s");
|
||||
vpadCircle.css("-webkit-transition", "0s");
|
||||
if (event.changedTouches) {
|
||||
resetVpadState();
|
||||
|
||||
vpadTouchIdx = event.changedTouches[0].identifier;
|
||||
event.clientX = event.changedTouches[0].clientX;
|
||||
event.clientY = event.changedTouches[0].clientY;
|
||||
}
|
||||
vpadTouchDrag = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function handleVpadJoystickUp(event) {
|
||||
if (vpadTouchDrag === null) return;
|
||||
|
||||
vpadCircle.css("transition", ".2s");
|
||||
vpadCircle.css("-moz-transition", ".2s");
|
||||
vpadCircle.css("-webkit-transition", ".2s");
|
||||
vpadCircle.css("transform", "translate3d(0px, 0px, 0px)");
|
||||
vpadCircle.css("-moz-transform", "translate3d(0px, 0px, 0px)");
|
||||
vpadCircle.css("-webkit-transform", "translate3d(0px, 0px, 0px)");
|
||||
|
||||
resetVpadState();
|
||||
}
|
||||
|
||||
|
||||
function handleVpadJoystickMove(event) {
|
||||
if (vpadTouchDrag === null) return;
|
||||
|
||||
if (event.changedTouches) {
|
||||
// check if moving source is from other touch?
|
||||
for (var i = 0; i < event.changedTouches.length; i++) {
|
||||
if (event.changedTouches[i].identifier === vpadTouchIdx) {
|
||||
event.clientX = event.changedTouches[i].clientX;
|
||||
event.clientY = event.changedTouches[i].clientY;
|
||||
}
|
||||
}
|
||||
if (event.clientX === undefined || event.clientY === undefined)
|
||||
return;
|
||||
}
|
||||
|
||||
var xDiff = event.clientX - vpadTouchDrag.x;
|
||||
var yDiff = event.clientY - vpadTouchDrag.y;
|
||||
var angle = Math.atan2(yDiff, xDiff);
|
||||
var distance = Math.min(MAX_DIFF, Math.hypot(xDiff, yDiff));
|
||||
var xNew = distance * Math.cos(angle);
|
||||
var yNew = distance * Math.sin(angle);
|
||||
|
||||
// check if screen is switched or not
|
||||
if (isLayoutSwitched) {
|
||||
tmp = xNew;
|
||||
xNew = yNew;
|
||||
yNew = -tmp;
|
||||
}
|
||||
|
||||
style = `translate(${xNew}px, ${yNew}px)`;
|
||||
vpadCircle.css("transform", style);
|
||||
vpadCircle.css("-webkit-transform", style);
|
||||
vpadCircle.css("-moz-transform", style);
|
||||
|
||||
var xRatio = xNew / MAX_DIFF;
|
||||
var yRatio = yNew / MAX_DIFF;
|
||||
checkVpadState("left", xRatio <= -0.5);
|
||||
checkVpadState("right", xRatio >= 0.5);
|
||||
checkVpadState("up", yRatio <= -0.5);
|
||||
checkVpadState("down", yRatio >= 0.5);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// touch/mouse events for dpad. mouseup events is binded to window.
|
||||
vpadHolder.on('mousedown', handleVpadJoystickDown);
|
||||
vpadHolder.on('touchstart', handleVpadJoystickDown);
|
||||
vpadHolder.on('touchend', handleVpadJoystickUp);
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Right side - Control buttons
|
||||
*/
|
||||
|
||||
function handleButtonDown(event) {
|
||||
checkVpadState($(this).attr("value"), true);
|
||||
// add touchIdx?
|
||||
}
|
||||
|
||||
function handleButtonUp(event) {
|
||||
checkVpadState($(this).attr("value"), false);
|
||||
}
|
||||
|
||||
|
||||
// touch/mouse events for control buttons. mouseup events is binded to window.
|
||||
$(".btn").on("mousedown", handleButtonDown);
|
||||
$(".btn").on("touchstart", handleButtonDown);
|
||||
$(".btn").on("touchend", handleButtonUp);
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Touch menu
|
||||
*/
|
||||
|
||||
let menuTouchIdx = null;
|
||||
let menuTouchDrag = null;
|
||||
let menuTouchTime = null;
|
||||
|
||||
function handleMenuDown(event) {
|
||||
// Identify of touch point
|
||||
if (event.changedTouches) {
|
||||
menuTouchIdx = event.changedTouches[0].identifier;
|
||||
event.clientX = event.changedTouches[0].clientX;
|
||||
event.clientY = event.changedTouches[0].clientY;
|
||||
}
|
||||
|
||||
menuTouchDrag = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
};
|
||||
|
||||
menuTouchTime = Date.now();
|
||||
}
|
||||
|
||||
function handleMenuMove(event) {
|
||||
if (menuTouchDrag === null) return;
|
||||
|
||||
if (event.changedTouches) {
|
||||
// check if moving source is from other touch?
|
||||
for (var i = 0; i < event.changedTouches.length; i++) {
|
||||
if (event.changedTouches[i].identifier === menuTouchIdx) {
|
||||
event.clientX = event.changedTouches[i].clientX;
|
||||
event.clientY = event.changedTouches[i].clientY;
|
||||
}
|
||||
}
|
||||
if (event.clientX == undefined || event.clientY == undefined)
|
||||
return;
|
||||
}
|
||||
|
||||
var listbox = $("#menu-container");
|
||||
listbox.css("transition", "");
|
||||
listbox.css("-moz-transition", "");
|
||||
listbox.css("-webkit-transition", "");
|
||||
if (isLayoutSwitched) {
|
||||
listbox.css("top", `${menuTop - (-menuTouchDrag.x + event.clientX)}px`);
|
||||
} else {
|
||||
listbox.css("top", `${menuTop - (menuTouchDrag.y - event.clientY)}px`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleMenuUp(event) {
|
||||
if (menuTouchDrag === null) return;
|
||||
if (event.changedTouches) {
|
||||
if (event.changedTouches[0].identifier !== menuTouchIdx)
|
||||
return;
|
||||
event.clientX = event.changedTouches[0].clientX;
|
||||
event.clientY = event.changedTouches[0].clientY;
|
||||
}
|
||||
|
||||
var interval = Date.now() - menuTouchTime; // 100ms?
|
||||
if (isLayoutSwitched) {
|
||||
newY = -menuTouchDrag.x + event.clientX;
|
||||
} else {
|
||||
newY = menuTouchDrag.y - event.clientY;
|
||||
}
|
||||
|
||||
if (interval < 200) {
|
||||
// calc velo
|
||||
newY = newY/ interval * 250;
|
||||
}
|
||||
// current item?
|
||||
menuTop -= newY;
|
||||
idx = Math.round((menuTop - MENU_TOP_POSITION) / -36);
|
||||
pickGame(idx);
|
||||
|
||||
menuTouchDrag = null;
|
||||
}
|
||||
|
||||
|
||||
// Bind events for menu
|
||||
$("#menu-screen").on("mousedown", handleMenuDown);
|
||||
$("#menu-screen").on("touchstart", handleMenuDown);
|
||||
$("#menu-screen").on("touchend", handleMenuUp);
|
||||
|
||||
|
||||
|
||||
/*
|
||||
Common events
|
||||
*/
|
||||
|
||||
function handleWindowMove(event) {
|
||||
event.preventDefault();
|
||||
handleVpadJoystickMove(event);
|
||||
handleMenuMove(event);
|
||||
|
||||
// moving touch
|
||||
if (event.changedTouches) {
|
||||
for (var i = 0; i < event.changedTouches.length; i++) {
|
||||
if (event.changedTouches[i].identifier !== menuTouchIdx && event.changedTouches[i].identifier !== vpadTouchIdx) {
|
||||
// check class
|
||||
|
||||
var elem = document.elementFromPoint(event.changedTouches[i].clientX, event.changedTouches[i].clientY);
|
||||
if (elem.classList.contains("btn")) {
|
||||
$(elem).trigger("touchstart");
|
||||
} else {
|
||||
$(".btn").trigger("touchend");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowUp(event) {
|
||||
// unmute when there is user interaction
|
||||
document.getElementById("game-screen").muted = false;
|
||||
|
||||
handleVpadJoystickUp(event);
|
||||
handleMenuUp(event);
|
||||
$(".btn").trigger("touchend");
|
||||
}
|
||||
|
||||
// Bind events for window
|
||||
$(window).on("mousemove", handleWindowMove);
|
||||
window.addEventListener("touchmove", handleWindowMove, {passive: false});
|
||||
$(window).on("mouseup", handleWindowUp);
|
||||
68
web/js/global.js
vendored
68
web/js/global.js
vendored
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
Global Constants
|
||||
*/
|
||||
const DEBUG = false;
|
||||
|
||||
const KEY_BIT = ["a", "b", "x", "y", "select", "start", "up", "down", "left", "right"];
|
||||
const INPUT_FPS = 100;
|
||||
const INPUT_STATE_PACKET = 1;
|
||||
const PINGPONGPS = 5;
|
||||
|
||||
const MENU_TOP_POSITION = 102;
|
||||
const ICE_TIMEOUT = 2000;
|
||||
|
||||
/*
|
||||
Global variables
|
||||
*/
|
||||
|
||||
|
||||
// Game state
|
||||
let screenState = "loader";
|
||||
let gameList = [];
|
||||
let gameIdx = 9; // contra
|
||||
let gamePickerTimer = null;
|
||||
let roomID = null;
|
||||
|
||||
|
||||
// Game controller state
|
||||
let keyState = {
|
||||
// control
|
||||
a: false,
|
||||
b: false,
|
||||
x: false,
|
||||
y: false,
|
||||
start: false,
|
||||
select: false,
|
||||
|
||||
// dpad
|
||||
up: false,
|
||||
down: false,
|
||||
left: false,
|
||||
right: false
|
||||
}
|
||||
|
||||
let unchangePacket = INPUT_STATE_PACKET;
|
||||
let gameInputTimer = null;
|
||||
|
||||
|
||||
// Network state
|
||||
let pc, inputChannel;
|
||||
let localSessionDescription = "";
|
||||
let remoteSessionDescription = "";
|
||||
let conn;
|
||||
|
||||
|
||||
// Touch menu state
|
||||
let menuDrag = null;
|
||||
let menuTop = MENU_TOP_POSITION;
|
||||
|
||||
|
||||
// Screen state
|
||||
let isLayoutSwitched = false;
|
||||
|
||||
var gameReady = false;
|
||||
var inputReady = false;
|
||||
var audioReady = false;
|
||||
var iceSuccess = true;
|
||||
var iceSent = false; // TODO: set to false in some init event
|
||||
var defaultICE = [{urls: "stun:stun.l.google.com:19302"}]
|
||||
48
web/js/init.js
vendored
48
web/js/init.js
vendored
|
|
@ -1,43 +1,13 @@
|
|||
log.setLevel('debug');
|
||||
|
||||
// Window rerender / rotate screen if needed
|
||||
function fixScreenLayout() {
|
||||
|
||||
var targetWidth = $(document).width() * 0.9;
|
||||
var targetHeight = $(document).height() * 0.9;
|
||||
$(document).ready(() => {
|
||||
env.display().fixScreenLayout();
|
||||
|
||||
// mobile == full screen
|
||||
if (getOS() === "android") {
|
||||
var targetWidth = $(document).width();
|
||||
var targetHeight = $(document).height();
|
||||
}
|
||||
keyboard.init();
|
||||
joystick.init();
|
||||
touch.init();
|
||||
|
||||
// Should have maximum box for desktop?
|
||||
// targetWidth = 800; targetHeight = 600; // test on desktop
|
||||
|
||||
fixElementLayout($("#gamebody"), targetWidth, targetHeight);
|
||||
|
||||
var elem = $("#ribbon");
|
||||
var st = "";
|
||||
if (isLayoutSwitched) {
|
||||
var st = "rotate(90deg)";
|
||||
elem.css("bottom", 0);
|
||||
elem.css("top", "");
|
||||
} else {
|
||||
elem.css("bottom", "");
|
||||
elem.css("top", 0);
|
||||
}
|
||||
elem.css("transform", st);
|
||||
elem.css("-webkit-transform", st);
|
||||
elem.css("-moz-transform", st);
|
||||
}
|
||||
|
||||
$(window).on("resize", fixScreenLayout);
|
||||
$(window).on("orientationchange", fixScreenLayout);
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
fixScreenLayout();
|
||||
|
||||
$("#room-txt").val(roomID);
|
||||
const roomId = room.loadMaybe();
|
||||
// if from URL -> start game immediately!
|
||||
socket.init(roomId);
|
||||
});
|
||||
|
||||
|
|
|
|||
75
web/js/input/input.js
vendored
Normal file
75
web/js/input/input.js
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
const input = (() => {
|
||||
const INPUT_HZ = 100;
|
||||
const INPUT_STATE_PACKET = 1;
|
||||
const KEY_BITS = [KEY.A, KEY.B, KEY.X, KEY.Y, KEY.SELECT, KEY.START, KEY.UP, KEY.DOWN, KEY.LEFT, KEY.RIGHT];
|
||||
|
||||
let gameInputTimer = null;
|
||||
let unchangePacket = INPUT_STATE_PACKET;
|
||||
|
||||
// Game controller state
|
||||
let keyState = {
|
||||
// control
|
||||
[KEY.A]: false,
|
||||
[KEY.B]: false,
|
||||
[KEY.X]: false,
|
||||
[KEY.Y]: false,
|
||||
[KEY.START]: false,
|
||||
[KEY.SELECT]: false,
|
||||
// dpad
|
||||
[KEY.UP]: false,
|
||||
[KEY.DOWN]: false,
|
||||
[KEY.LEFT]: false,
|
||||
[KEY.RIGHT]: false
|
||||
};
|
||||
|
||||
const poll = () => {
|
||||
return {
|
||||
enable: () => {
|
||||
if (gameInputTimer !== null) return;
|
||||
|
||||
const inputPollInterval = 1000 / INPUT_HZ;
|
||||
log.info(`[input] setting input polling interval to ${inputPollInterval}ms`);
|
||||
gameInputTimer = setInterval(sendKeyState, inputPollInterval)
|
||||
},
|
||||
disable: () => {
|
||||
if (gameInputTimer === null) return;
|
||||
|
||||
log.info('[input] stop game input timer');
|
||||
clearInterval(gameInputTimer);
|
||||
gameInputTimer = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// relatively slow method
|
||||
const sendKeyState = () => {
|
||||
// check if state is changed
|
||||
if (unchangePacket > 0) {
|
||||
// pack keys state
|
||||
let bits = '';
|
||||
KEY_BITS.slice().reverse().forEach(elem => {
|
||||
bits += keyState[elem] ? 1 : 0;
|
||||
});
|
||||
let data = parseInt(bits, 2);
|
||||
|
||||
let arrBuf = new Uint8Array(2);
|
||||
arrBuf[0] = data & ((1 << 8) - 1);
|
||||
arrBuf[1] = data >> 8;
|
||||
event.pub(KEY_STATE_UPDATED, arrBuf);
|
||||
|
||||
unchangePacket--;
|
||||
}
|
||||
};
|
||||
|
||||
const setKeyState = (name, state) => {
|
||||
if (name in keyState) {
|
||||
keyState[name] = state;
|
||||
unchangePacket = INPUT_STATE_PACKET;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
poll: poll,
|
||||
setKeyState: setKeyState
|
||||
}
|
||||
})(event, KEY);
|
||||
200
web/js/input/joystick.js
vendored
Normal file
200
web/js/input/joystick.js
vendored
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
/**
|
||||
* Joystick controls.
|
||||
*
|
||||
* cross == a <--> a
|
||||
* circle == b <--> b
|
||||
* square == x <--> start
|
||||
* triangle == y <--> select
|
||||
* share <--> load
|
||||
* option <--> save
|
||||
* L2 == LT <--> full
|
||||
* R2 == RT <--> quit
|
||||
* dpad <--> up down left right
|
||||
* axis 0, 1 <--> second dpad
|
||||
*
|
||||
* change full to help (temporary)
|
||||
*
|
||||
* @version 1
|
||||
*/
|
||||
const joystick = (() => {
|
||||
let joystickMap;
|
||||
let joystickState;
|
||||
let joystickIdx;
|
||||
let joystickTimer = null;
|
||||
|
||||
// check state for each axis -> dpad
|
||||
function checkJoystickAxisState(name, state) {
|
||||
if (joystickState[name] !== state) {
|
||||
joystickState[name] = state;
|
||||
event.pub(state === true ? KEY_PRESSED : KEY_RELEASED, {key: name});
|
||||
}
|
||||
}
|
||||
|
||||
// loop timer for checking joystick state
|
||||
function checkJoystickState() {
|
||||
let gamepad = navigator.getGamepads()[joystickIdx];
|
||||
if (gamepad) {
|
||||
// axis -> dpad
|
||||
let corX = gamepad.axes[0]; // -1 -> 1, left -> right
|
||||
let corY = gamepad.axes[1]; // -1 -> 1, up -> down
|
||||
checkJoystickAxisState(KEY.LEFT, corX <= -0.5);
|
||||
checkJoystickAxisState(KEY.RIGHT, corX >= 0.5);
|
||||
checkJoystickAxisState(KEY.UP, corY <= -0.5);
|
||||
checkJoystickAxisState(KEY.DOWN, corY >= 0.5);
|
||||
|
||||
// normal button map
|
||||
Object.keys(joystickMap).forEach(function (btnIdx) {
|
||||
const buttonState = gamepad.buttons[btnIdx];
|
||||
|
||||
const isPressed = navigator.webkitGetGamepads ? buttonState === 1 :
|
||||
buttonState.value > 0 || buttonState.pressed === true;
|
||||
|
||||
if (joystickState[btnIdx] !== isPressed) {
|
||||
joystickState[btnIdx] = isPressed;
|
||||
event.pub(isPressed === true ? KEY_PRESSED : KEY_RELEASED, {key: joystickMap[btnIdx]});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// we only capture the last plugged joystick
|
||||
const onGamepadConnected = (event) => {
|
||||
let gamepad = event.gamepad;
|
||||
log.info(`Gamepad connected at index ${gamepad.index}: ${gamepad.id}. ${gamepad.buttons.length} buttons, ${gamepad.axes.length} axes.`);
|
||||
|
||||
joystickIdx = gamepad.index;
|
||||
|
||||
// Ref: https://github.com/giongto35/cloud-game/issues/14
|
||||
// get mapping first (default KeyMap2)
|
||||
let os = env.getOs();
|
||||
let browser = env.getBrowser();
|
||||
|
||||
if (os === 'android') {
|
||||
// default of android is KeyMap1
|
||||
joystickMap = {
|
||||
2: KEY.A,
|
||||
0: KEY.B,
|
||||
3: KEY.START,
|
||||
4: KEY.SELECT,
|
||||
10: KEY.LOAD,
|
||||
11: KEY.SAVE,
|
||||
8: KEY.HELP,
|
||||
9: KEY.QUIT,
|
||||
12: KEY.UP,
|
||||
13: KEY.DOWN,
|
||||
14: KEY.LEFT,
|
||||
15: KEY.RIGHT
|
||||
};
|
||||
} else {
|
||||
// default of other OS is KeyMap2
|
||||
joystickMap = {
|
||||
0: KEY.A,
|
||||
1: KEY.B,
|
||||
2: KEY.START,
|
||||
3: KEY.SELECT,
|
||||
8: KEY.LOAD,
|
||||
9: KEY.SAVE,
|
||||
6: KEY.HELP,
|
||||
7: KEY.QUIT,
|
||||
12: KEY.UP,
|
||||
13: KEY.DOWN,
|
||||
14: KEY.LEFT,
|
||||
15: KEY.RIGHT
|
||||
};
|
||||
}
|
||||
|
||||
if (os === 'android' && (browser === 'firefox' || browser === 'uc')) { //KeyMap2
|
||||
joystickMap = {
|
||||
0: KEY.A,
|
||||
1: KEY.B,
|
||||
2: KEY.START,
|
||||
3: KEY.SELECT,
|
||||
8: KEY.LOAD,
|
||||
9: KEY.SAVE,
|
||||
6: KEY.HELP,
|
||||
7: KEY.QUIT,
|
||||
12: KEY.UP,
|
||||
13: KEY.DOWN,
|
||||
14: KEY.LEFT,
|
||||
15: KEY.RIGHT
|
||||
};
|
||||
}
|
||||
|
||||
if (os === 'win' && browser === 'firefox') { //KeyMap3
|
||||
joystickMap = {
|
||||
1: KEY.A,
|
||||
2: KEY.B,
|
||||
0: KEY.START,
|
||||
3: KEY.SELECT,
|
||||
8: KEY.LOAD,
|
||||
9: KEY.SAVE,
|
||||
6: KEY.HELP,
|
||||
7: KEY.QUIT
|
||||
};
|
||||
}
|
||||
|
||||
if (os === 'mac' && browser === 'safari') { //KeyMap4
|
||||
joystickMap = {
|
||||
1: KEY.A,
|
||||
2: KEY.B,
|
||||
0: KEY.START,
|
||||
3: KEY.SELECT,
|
||||
8: KEY.LOAD,
|
||||
9: KEY.SAVE,
|
||||
6: KEY.HELP,
|
||||
7: KEY.QUIT,
|
||||
14: KEY.UP,
|
||||
15: KEY.DOWN,
|
||||
16: KEY.LEFT,
|
||||
17: KEY.RIGHT
|
||||
};
|
||||
}
|
||||
|
||||
if (os === 'mac' && browser === 'firefox') { //KeyMap5
|
||||
joystickMap = {
|
||||
1: KEY.A,
|
||||
2: KEY.B,
|
||||
0: KEY.START,
|
||||
3: KEY.SELECT,
|
||||
8: KEY.LOAD,
|
||||
9: KEY.SAVE,
|
||||
6: KEY.HELP,
|
||||
7: KEY.QUIT,
|
||||
14: KEY.UP,
|
||||
15: KEY.DOWN,
|
||||
16: KEY.LEFT,
|
||||
17: KEY.RIGHT
|
||||
};
|
||||
}
|
||||
|
||||
// reset state
|
||||
joystickState = {[KEY.LEFT]: false, [KEY.RIGHT]: false, [KEY.UP]: false, [KEY.DOWN]: false};
|
||||
Object.keys(joystickMap).forEach(function (btnIdx) {
|
||||
joystickState[btnIdx] = false;
|
||||
});
|
||||
|
||||
// looper, too intense?
|
||||
if (joystickTimer !== null) {
|
||||
clearInterval(joystickTimer);
|
||||
}
|
||||
|
||||
joystickTimer = setInterval(checkJoystickState, 10); // miliseconds per hit
|
||||
event.pub(GAMEPAD_CONNECTED);
|
||||
};
|
||||
|
||||
return {
|
||||
init: () => {
|
||||
// we only capture the last plugged joystick
|
||||
window.addEventListener('gamepadconnected', onGamepadConnected);
|
||||
|
||||
// disconnected event is triggered
|
||||
window.addEventListener('gamepaddisconnected', (event) => {
|
||||
clearInterval(joystickTimer);
|
||||
log.info(`Gamepad disconnected at index ${event.gamepad.index}`);
|
||||
event.pub(GAMEPAD_DISCONNECTED);
|
||||
});
|
||||
|
||||
log.info('[input] joystick has been initialized');
|
||||
}
|
||||
}
|
||||
})(event, env, KEY, navigator, window);
|
||||
40
web/js/input/keyboard.js
vendored
Normal file
40
web/js/input/keyboard.js
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Keyboard controls.
|
||||
*
|
||||
* @version 1
|
||||
*/
|
||||
const keyboard = (() => {
|
||||
|
||||
const KEYBOARD_MAP = {
|
||||
37: KEY.LEFT,
|
||||
38: KEY.UP,
|
||||
39: KEY.RIGHT,
|
||||
40: KEY.DOWN,
|
||||
90: KEY.A, // z
|
||||
88: KEY.B, // x
|
||||
67: KEY.X, // c
|
||||
86: KEY.Y, // v
|
||||
13: KEY.START, // enter
|
||||
16: KEY.SELECT, // shift
|
||||
// non-game
|
||||
81: KEY.QUIT, // q
|
||||
83: KEY.SAVE, // s
|
||||
87: KEY.JOIN, // w
|
||||
65: KEY.LOAD, // a
|
||||
70: KEY.FULL, // f
|
||||
72: KEY.HELP, // h
|
||||
};
|
||||
|
||||
const onKey = (code, callback) => {
|
||||
if (code in KEYBOARD_MAP) callback(KEYBOARD_MAP[code]);
|
||||
};
|
||||
|
||||
return {
|
||||
init: () => {
|
||||
const body = $('body');
|
||||
body.on('keyup', ev => onKey(ev.keyCode, key => event.pub(KEY_RELEASED, {key: key})));
|
||||
body.on('keydown', ev => onKey(ev.keyCode, key => event.pub(KEY_PRESSED, {key: key})));
|
||||
log.info('[input] keyboard has been initialized');
|
||||
}
|
||||
}
|
||||
})(event, KEY);
|
||||
20
web/js/input/keys.js
vendored
Normal file
20
web/js/input/keys.js
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const KEY = (() => {
|
||||
return {
|
||||
A: 'a',
|
||||
B: 'b',
|
||||
X: 'x',
|
||||
Y: 'y',
|
||||
START: 'start',
|
||||
SELECT: 'select',
|
||||
LOAD: 'load',
|
||||
SAVE: 'save',
|
||||
HELP: 'help',
|
||||
JOIN: 'join',
|
||||
FULL: 'full',
|
||||
QUIT: 'quit',
|
||||
UP: 'up',
|
||||
DOWN: 'down',
|
||||
LEFT: 'left',
|
||||
RIGHT: 'right',
|
||||
}
|
||||
})();
|
||||
250
web/js/input/touch.js
vendored
Normal file
250
web/js/input/touch.js
vendored
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
/**
|
||||
* Touch controls.
|
||||
*
|
||||
* Virtual Gamepad / Joystick
|
||||
* Left panel - Dpad
|
||||
*
|
||||
* @link https://jsfiddle.net/aa0et7tr/5/
|
||||
* @version 1
|
||||
*/
|
||||
const touch = (() => {
|
||||
const MAX_DIFF = 20; // radius of circle boundary
|
||||
|
||||
// vpad state, use for mouse button down
|
||||
let vpadState = {[KEY.UP]: false, [KEY.DOWN]: false, [KEY.LEFT]: false, [KEY.RIGHT]: false};
|
||||
|
||||
let vpadTouchIdx = null;
|
||||
let vpadTouchDrag = null;
|
||||
let vpadHolder = $("#circle-pad-holder");
|
||||
let vpadCircle = $("#circle-pad");
|
||||
|
||||
const window_ = $(window);
|
||||
const buttons = $(".btn");
|
||||
const dpad = $(".dpad");
|
||||
|
||||
function resetVpadState() {
|
||||
// trigger up event?
|
||||
checkVpadState(KEY.UP, false);
|
||||
checkVpadState(KEY.DOWN, false);
|
||||
checkVpadState(KEY.LEFT, false);
|
||||
checkVpadState(KEY.RIGHT, false);
|
||||
|
||||
vpadTouchDrag = null;
|
||||
vpadTouchIdx = null;
|
||||
dpad.removeClass('pressed');
|
||||
}
|
||||
|
||||
function checkVpadState(axis, state) {
|
||||
if (state !== vpadState[axis]) {
|
||||
vpadState[axis] = state;
|
||||
event.pub(state ? KEY_PRESSED : KEY_RELEASED, {key: axis});
|
||||
}
|
||||
}
|
||||
|
||||
function handleVpadJoystickDown(event) {
|
||||
vpadCircle.css('transition', '0s');
|
||||
vpadCircle.css('-moz-transition', '0s');
|
||||
vpadCircle.css('-webkit-transition', '0s');
|
||||
|
||||
if (event.changedTouches) {
|
||||
resetVpadState();
|
||||
vpadTouchIdx = event.changedTouches[0].identifier;
|
||||
event.clientX = event.changedTouches[0].clientX;
|
||||
event.clientY = event.changedTouches[0].clientY;
|
||||
}
|
||||
|
||||
vpadTouchDrag = {x: event.clientX, y: event.clientY};
|
||||
}
|
||||
|
||||
function handleVpadJoystickUp() {
|
||||
if (vpadTouchDrag === null) return;
|
||||
|
||||
vpadCircle.css('transition', '.2s');
|
||||
vpadCircle.css('-moz-transition', '.2s');
|
||||
vpadCircle.css('-webkit-transition', '.2s');
|
||||
vpadCircle.css('transform', 'translate3d(0px, 0px, 0px)');
|
||||
vpadCircle.css('-moz-transform', 'translate3d(0px, 0px, 0px)');
|
||||
vpadCircle.css('-webkit-transform', 'translate3d(0px, 0px, 0px)');
|
||||
|
||||
resetVpadState();
|
||||
}
|
||||
|
||||
function handleVpadJoystickMove(event) {
|
||||
if (vpadTouchDrag === null) return;
|
||||
|
||||
if (event.changedTouches) {
|
||||
// check if moving source is from other touch?
|
||||
for (let i = 0; i < event.changedTouches.length; i++) {
|
||||
if (event.changedTouches[i].identifier === vpadTouchIdx) {
|
||||
event.clientX = event.changedTouches[i].clientX;
|
||||
event.clientY = event.changedTouches[i].clientY;
|
||||
}
|
||||
}
|
||||
if (event.clientX === undefined || event.clientY === undefined)
|
||||
return;
|
||||
}
|
||||
|
||||
let xDiff = event.clientX - vpadTouchDrag.x;
|
||||
let yDiff = event.clientY - vpadTouchDrag.y;
|
||||
let angle = Math.atan2(yDiff, xDiff);
|
||||
let distance = Math.min(MAX_DIFF, Math.hypot(xDiff, yDiff));
|
||||
let xNew = distance * Math.cos(angle);
|
||||
let yNew = distance * Math.sin(angle);
|
||||
|
||||
if (env.display().isLayoutSwitched) {
|
||||
let tmp = xNew;
|
||||
xNew = yNew;
|
||||
yNew = -tmp;
|
||||
}
|
||||
|
||||
let style = `translate(${xNew}px, ${yNew}px)`;
|
||||
vpadCircle.css('transform', style);
|
||||
vpadCircle.css('-webkit-transform', style);
|
||||
vpadCircle.css('-moz-transform', style);
|
||||
|
||||
let xRatio = xNew / MAX_DIFF;
|
||||
let yRatio = yNew / MAX_DIFF;
|
||||
checkVpadState(KEY.LEFT, xRatio <= -0.5);
|
||||
checkVpadState(KEY.RIGHT, xRatio >= 0.5);
|
||||
checkVpadState(KEY.UP, yRatio <= -0.5);
|
||||
checkVpadState(KEY.DOWN, yRatio >= 0.5);
|
||||
}
|
||||
|
||||
/*
|
||||
Right side - Control buttons
|
||||
*/
|
||||
|
||||
function handleButtonDown() {
|
||||
checkVpadState($(this).attr('value'), true);
|
||||
// add touchIdx?
|
||||
}
|
||||
|
||||
function handleButtonUp() {
|
||||
checkVpadState($(this).attr('value'), false);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Touch menu
|
||||
*/
|
||||
|
||||
let menuTouchIdx = null;
|
||||
let menuTouchDrag = null;
|
||||
let menuTouchTime = null;
|
||||
|
||||
function handleMenuDown(event) {
|
||||
// Identify of touch point
|
||||
if (event.changedTouches) {
|
||||
menuTouchIdx = event.changedTouches[0].identifier;
|
||||
event.clientX = event.changedTouches[0].clientX;
|
||||
event.clientY = event.changedTouches[0].clientY;
|
||||
}
|
||||
|
||||
menuTouchDrag = {x: event.clientX, y: event.clientY,};
|
||||
menuTouchTime = Date.now();
|
||||
}
|
||||
|
||||
function handleMenuMove(evt) {
|
||||
if (menuTouchDrag === null) return;
|
||||
|
||||
if (evt.changedTouches) {
|
||||
// check if moving source is from other touch?
|
||||
for (let i = 0; i < evt.changedTouches.length; i++) {
|
||||
if (evt.changedTouches[i].identifier === menuTouchIdx) {
|
||||
evt.clientX = evt.changedTouches[i].clientX;
|
||||
evt.clientY = evt.changedTouches[i].clientY;
|
||||
}
|
||||
}
|
||||
if (evt.clientX === undefined || evt.clientY === undefined)
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = env.display().isLayoutSwitched ? evt.clientX - menuTouchDrag.x : menuTouchDrag.y - evt.clientY;
|
||||
event.pub(MENU_PRESSED, pos);
|
||||
}
|
||||
|
||||
function handleMenuUp(evt) {
|
||||
if (menuTouchDrag === null) return;
|
||||
if (evt.changedTouches) {
|
||||
if (evt.changedTouches[0].identifier !== menuTouchIdx)
|
||||
return;
|
||||
evt.clientX = evt.changedTouches[0].clientX;
|
||||
evt.clientY = evt.changedTouches[0].clientY;
|
||||
}
|
||||
|
||||
let newY = env.display().isLayoutSwitched ? -menuTouchDrag.x + evt.clientX : menuTouchDrag.y - evt.clientY;
|
||||
|
||||
let interval = Date.now() - menuTouchTime; // 100ms?
|
||||
if (interval < 200) {
|
||||
// calc velo
|
||||
newY = newY / interval * 250;
|
||||
}
|
||||
|
||||
// current item?
|
||||
event.pub(MENU_RELEASED, newY);
|
||||
menuTouchDrag = null;
|
||||
}
|
||||
|
||||
/*
|
||||
Common events
|
||||
*/
|
||||
function handleWindowMove(event) {
|
||||
event.preventDefault();
|
||||
handleVpadJoystickMove(event);
|
||||
handleMenuMove(event);
|
||||
|
||||
// moving touch
|
||||
if (event.changedTouches) {
|
||||
for (let i = 0; i < event.changedTouches.length; i++) {
|
||||
if (event.changedTouches[i].identifier !== menuTouchIdx && event.changedTouches[i].identifier !== vpadTouchIdx) {
|
||||
// check class
|
||||
|
||||
let elem = document.elementFromPoint(event.changedTouches[i].clientX, event.changedTouches[i].clientY);
|
||||
if (elem.classList.contains('btn')) {
|
||||
$(elem).trigger('touchstart');
|
||||
} else {
|
||||
buttons.trigger('touchend');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowUp(ev) {
|
||||
handleVpadJoystickUp(ev);
|
||||
handleMenuUp(ev);
|
||||
buttons.trigger('touchend');
|
||||
}
|
||||
|
||||
// touch/mouse events for control buttons. mouseup events is binded to window.
|
||||
buttons.on('mousedown', handleButtonDown);
|
||||
buttons.on('touchstart', handleButtonDown);
|
||||
buttons.on('touchend', handleButtonUp);
|
||||
|
||||
// touch/mouse events for dpad. mouseup events is binded to window.
|
||||
vpadHolder.on('mousedown', handleVpadJoystickDown);
|
||||
vpadHolder.on('touchstart', handleVpadJoystickDown);
|
||||
vpadHolder.on('touchend', handleVpadJoystickUp);
|
||||
|
||||
// Bind events for menu
|
||||
// TODO change this flow
|
||||
event.pub(MENU_HANDLER_ATTACHED, {event: 'mousedown', handler: handleMenuDown});
|
||||
event.pub(MENU_HANDLER_ATTACHED, {event: 'touchstart', handler: handleMenuDown});
|
||||
event.pub(MENU_HANDLER_ATTACHED, {event: 'touchend', handler: handleMenuUp});
|
||||
|
||||
return {
|
||||
init: () => {
|
||||
// add buttons into the state 🤦
|
||||
$('.btn, .btn-big').each((_, el) => {
|
||||
vpadState[$(el).attr('value')] = false;
|
||||
});
|
||||
|
||||
// Bind events for window
|
||||
window_.on('mousemove', handleWindowMove);
|
||||
window_[0].addEventListener('touchmove', handleWindowMove, {passive: false});
|
||||
window_.on('mouseup', handleWindowUp);
|
||||
|
||||
log.info('[input] touch input has been initialized');
|
||||
}
|
||||
}
|
||||
})($, document, event, KEY, window);
|
||||
2
web/js/jquery-3.3.1.min.js
vendored
2
web/js/jquery-3.3.1.min.js
vendored
File diff suppressed because one or more lines are too long
2
web/js/lib/jquery-3.4.1.min.js
vendored
Normal file
2
web/js/lib/jquery-3.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
31
web/js/libopus.js
vendored
31
web/js/libopus.js
vendored
File diff suppressed because one or more lines are too long
21
web/js/log.js
vendored
Normal file
21
web/js/log.js
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
const log = (() => {
|
||||
let level = 'info';
|
||||
const levels = {'trace': 0, 'debug': 1, 'error': 2, 'info': 3};
|
||||
|
||||
const atLeast = (lv) => (levels[lv] || -1) >= levels[level];
|
||||
|
||||
return {
|
||||
info: function () {
|
||||
atLeast('info') && console.info.apply(null, arguments)
|
||||
},
|
||||
debug: function () {
|
||||
atLeast('debug') && console.debug.apply(null, arguments)
|
||||
},
|
||||
error: function () {
|
||||
atLeast('error') && console.error.apply(null, arguments)
|
||||
},
|
||||
setLevel: (level_) => {
|
||||
level = level_
|
||||
}
|
||||
}
|
||||
})();
|
||||
29
web/js/network/ajax.js
vendored
Normal file
29
web/js/network/ajax.js
vendored
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* AJAX request module.
|
||||
* @version 1
|
||||
*/
|
||||
const ajax = (() => {
|
||||
const defaultTimeout = 10000;
|
||||
|
||||
return {
|
||||
fetch: (url, options, timeout = defaultTimeout) => new Promise((resolve, reject) => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
const allOptions = Object.assign({}, options, signal);
|
||||
|
||||
// fetch(url, {...options, signal})
|
||||
fetch(url, allOptions)
|
||||
.then(resolve, () => {
|
||||
controller.abort();
|
||||
return reject
|
||||
});
|
||||
|
||||
// auto abort when a timeout reached
|
||||
setTimeout(() => {
|
||||
controller.abort();
|
||||
reject();
|
||||
}, timeout);
|
||||
}),
|
||||
defaultTimeoutMs: () => defaultTimeout
|
||||
}
|
||||
})();
|
||||
125
web/js/network/rtcp.js
vendored
Normal file
125
web/js/network/rtcp.js
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
/**
|
||||
* RTCP connection module.
|
||||
* @version 1
|
||||
*/
|
||||
const rtcp = (() => {
|
||||
let connection;
|
||||
let inputChannel;
|
||||
let mediaStream;
|
||||
|
||||
let connected = false;
|
||||
let inputReady = false;
|
||||
|
||||
const start = (iceservers) => {
|
||||
log.info(`[rtcp] <- received stunturn from worker ${iceservers}`);
|
||||
|
||||
connection = new RTCPeerConnection({
|
||||
iceServers: JSON.parse(iceservers)
|
||||
});
|
||||
|
||||
mediaStream = new MediaStream();
|
||||
|
||||
// input channel, ordered + reliable, id 0
|
||||
inputChannel = connection.createDataChannel('a', {ordered: true, negotiated: true, id: 0,});
|
||||
inputChannel.onopen = () => {
|
||||
log.debug('[rtcp] the input channel has opened');
|
||||
inputReady = true;
|
||||
event.pub(CONNECTION_READY)
|
||||
};
|
||||
inputChannel.onclose = () => log.debug('[rtcp] the input channel has closed');
|
||||
|
||||
connection.addTransceiver('video', {'direction': 'recvonly'});
|
||||
connection.addTransceiver('audio', {'direction': 'recvonly'});
|
||||
|
||||
connection.oniceconnectionstatechange = ice.onIceConnectionStateChange;
|
||||
connection.onicegatheringstatechange = ice.onIceStateChange;
|
||||
connection.onicecandidate = ice.onIcecandidate;
|
||||
connection.ontrack = event => mediaStream.addTrack(event.track);
|
||||
|
||||
connection.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true})
|
||||
.then(offer => {
|
||||
log.info(offer.sdp);
|
||||
connection.setLocalDescription(offer).catch(log.error);
|
||||
});
|
||||
};
|
||||
|
||||
const ice = (() => {
|
||||
let isGatheringDone = false;
|
||||
let timeForIceGathering;
|
||||
|
||||
const ICE_TIMEOUT = 2000;
|
||||
|
||||
const sendCandidates = () => {
|
||||
if (isGatheringDone) return;
|
||||
const session = btoa(JSON.stringify(connection.localDescription));
|
||||
const data = JSON.stringify({"sdp": session, "is_mobile": env.isMobileDevice()});
|
||||
socket.send({"id": "initwebrtc", "data": data});
|
||||
isGatheringDone = true;
|
||||
};
|
||||
|
||||
return {
|
||||
onIcecandidate: event => {
|
||||
if (event.candidate && !isGatheringDone) {
|
||||
log.info(JSON.stringify(event.candidate));
|
||||
} else {
|
||||
sendCandidates()
|
||||
}
|
||||
// TODO: Fix curPacketID
|
||||
},
|
||||
onIceStateChange: event => {
|
||||
switch (event.target.iceGatheringState) {
|
||||
case 'gathering':
|
||||
log.info('[rtcp] ice gathering');
|
||||
timeForIceGathering = setTimeout(() => {
|
||||
log.info(`[rtcp] ice gathering was aborted due to timeout ${ICE_TIMEOUT}ms`);
|
||||
sendCandidates();
|
||||
}, ICE_TIMEOUT);
|
||||
break;
|
||||
case 'complete':
|
||||
log.info('[rtcp] ice gathering completed');
|
||||
if (timeForIceGathering) {
|
||||
clearTimeout(timeForIceGathering);
|
||||
}
|
||||
}
|
||||
},
|
||||
onIceConnectionStateChange: () => {
|
||||
log.info(`[rtcp] <- iceConnectionState: ${connection.iceConnectionState}`);
|
||||
switch (connection.iceConnectionState) {
|
||||
case 'connected': {
|
||||
log.info('[rtcp] connected...');
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
case 'disconnected': {
|
||||
log.info('[rtcp] disconnected...');
|
||||
connected = false;
|
||||
event.pub(CONNECTION_CLOSED);
|
||||
break;
|
||||
}
|
||||
case 'failed': {
|
||||
log.error('[rtcp] connection failed, retry...');
|
||||
connected = false;
|
||||
connection.createOffer({iceRestart: true})
|
||||
.then(description => connection.setLocalDescription(description).catch(log.error))
|
||||
.catch(log.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
start: start,
|
||||
setRemoteDescription: (data, media) => {
|
||||
connection.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(data))))
|
||||
// set media object stream
|
||||
.then(() => {
|
||||
media.srcObject = mediaStream;
|
||||
})
|
||||
},
|
||||
input: (data) => inputChannel.send(data),
|
||||
isConnected: () => connected,
|
||||
isInputReady: () => inputReady
|
||||
}
|
||||
})(event, socket, env, log);
|
||||
98
web/js/network/socket.js
vendored
Normal file
98
web/js/network/socket.js
vendored
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* WebSocket connection module.
|
||||
*
|
||||
* Needs init() call.
|
||||
*
|
||||
* @version 1
|
||||
*/
|
||||
const socket = (() => {
|
||||
const pingIntervalMs = 1000 / 5;
|
||||
|
||||
let conn;
|
||||
let curPacketId = '';
|
||||
|
||||
const init = (roomId) => {
|
||||
conn = new WebSocket(`ws://${location.host}/ws${roomId ? `?room_id=${roomId}` : ''}`);
|
||||
|
||||
// Clear old roomID
|
||||
conn.onopen = () => {
|
||||
log.info('[ws] <- open connection');
|
||||
log.info(`[ws] -> setting ping interval to ${pingIntervalMs}ms`);
|
||||
// !to add destructor if SPA
|
||||
setInterval(ping, pingIntervalMs)
|
||||
};
|
||||
conn.onerror = error => log.error(`[ws] ${error}`);
|
||||
conn.onclose = () => log.info('[ws] closed');
|
||||
conn.onmessage = response => {
|
||||
const data = JSON.parse(response.data);
|
||||
const message = data.id;
|
||||
|
||||
if (message !== 'heartbeat') log.debug(`[ws] <- message '${message}' `, data);
|
||||
|
||||
switch (message) {
|
||||
case 'init':
|
||||
// TODO: Read from struct
|
||||
// init package has 2 part [stunturn, game1, game2, game3 ...]
|
||||
// const [stunturn, ...games] = data;
|
||||
let serverData = JSON.parse(data.data);
|
||||
event.pub(MEDIA_STREAM_INITIALIZED, {stunturn: serverData.shift(), games: serverData});
|
||||
break;
|
||||
case 'sdp':
|
||||
event.pub(MEDIA_STREAM_SDP_AVAILABLE, {sdp: data.data});
|
||||
break;
|
||||
case 'requestOffer':
|
||||
// !to remove? wtf
|
||||
curPacketId = data.packet_id;
|
||||
event.pub(MEDIA_STREAM_READY);
|
||||
break;
|
||||
case 'heartbeat':
|
||||
// reserved
|
||||
break;
|
||||
case 'start':
|
||||
event.pub(GAME_ROOM_AVAILABLE, {roomId: data.room_id});
|
||||
break;
|
||||
case 'save':
|
||||
event.pub(GAME_SAVED);
|
||||
break;
|
||||
case 'load':
|
||||
event.pub(GAME_LOADED);
|
||||
break;
|
||||
case 'checkLatency':
|
||||
curPacketId = data.packet_id;
|
||||
const addresses = data.data.split(',');
|
||||
event.pub(LATENCY_CHECK_REQUESTED, {packetId: curPacketId, addresses: addresses});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: format the package with time
|
||||
const ping = () => send({"id": "heartbeat", "data": Date.now().toString()});
|
||||
const send = (data) => conn.send(JSON.stringify(data));
|
||||
const latency = (workers, packetId) => send({
|
||||
"id": "checkLatency",
|
||||
"data": JSON.stringify(workers),
|
||||
"packet_id": packetId
|
||||
});
|
||||
const saveGame = () => send({"id": "save", "data": ""});
|
||||
const loadGame = () => send({"id": "load", "data": ""});
|
||||
const startGame = (gameName, isMobile, roomId, playerIndex) => send({
|
||||
"id": "start",
|
||||
"data": JSON.stringify({
|
||||
"game_name": gameName,
|
||||
"is_mobile": isMobile
|
||||
}),
|
||||
"room_id": roomId != null ? roomId : '',
|
||||
"player_index": playerIndex
|
||||
});
|
||||
const quitGame = (roomId) => send({"id": "quit", "data": "", "room_id": roomId});
|
||||
|
||||
return {
|
||||
init: init,
|
||||
send: send,
|
||||
latency: latency,
|
||||
saveGame: saveGame,
|
||||
loadGame: loadGame,
|
||||
startGame: startGame,
|
||||
quitGame: quitGame
|
||||
}
|
||||
})($, event, log);
|
||||
200
web/js/opus.js
vendored
200
web/js/opus.js
vendored
|
|
@ -1,200 +0,0 @@
|
|||
///<reference path="d.ts/asm.d.ts" />
|
||||
///<reference path="d.ts/libopus.d.ts" />
|
||||
var OpusApplication;
|
||||
(function (OpusApplication) {
|
||||
OpusApplication[OpusApplication["VoIP"] = 2048] = "VoIP";
|
||||
OpusApplication[OpusApplication["Audio"] = 2049] = "Audio";
|
||||
OpusApplication[OpusApplication["RestrictedLowDelay"] = 2051] = "RestrictedLowDelay";
|
||||
})(OpusApplication || (OpusApplication = {}));
|
||||
var OpusError;
|
||||
(function (OpusError) {
|
||||
OpusError[OpusError["OK"] = 0] = "OK";
|
||||
OpusError[OpusError["BadArgument"] = -1] = "BadArgument";
|
||||
OpusError[OpusError["BufferTooSmall"] = -2] = "BufferTooSmall";
|
||||
OpusError[OpusError["InternalError"] = -3] = "InternalError";
|
||||
OpusError[OpusError["InvalidPacket"] = -4] = "InvalidPacket";
|
||||
OpusError[OpusError["Unimplemented"] = -5] = "Unimplemented";
|
||||
OpusError[OpusError["InvalidState"] = -6] = "InvalidState";
|
||||
OpusError[OpusError["AllocFail"] = -7] = "AllocFail";
|
||||
})(OpusError || (OpusError = {}));
|
||||
var Opus = (function () {
|
||||
function Opus() {
|
||||
}
|
||||
Opus.getVersion = function () {
|
||||
var ptr = _opus_get_version_string();
|
||||
return Pointer_stringify(ptr);
|
||||
};
|
||||
Opus.getMaxFrameSize = function (numberOfStreams) {
|
||||
if (numberOfStreams === void 0) { numberOfStreams = 1; }
|
||||
return (1275 * 3 + 7) * numberOfStreams;
|
||||
};
|
||||
Opus.getMinFrameDuration = function () {
|
||||
return 2.5;
|
||||
};
|
||||
Opus.getMaxFrameDuration = function () {
|
||||
return 60;
|
||||
};
|
||||
Opus.validFrameDuration = function (x) {
|
||||
return [2.5, 5, 10, 20, 40, 60].some(function (element) {
|
||||
return element == x;
|
||||
});
|
||||
};
|
||||
Opus.getMaxSamplesPerChannel = function (sampling_rate) {
|
||||
return sampling_rate / 1000 * Opus.getMaxFrameDuration();
|
||||
};
|
||||
return Opus;
|
||||
})();
|
||||
var OpusEncoder = (function () {
|
||||
function OpusEncoder(sampling_rate, channels, app, frame_duration) {
|
||||
if (frame_duration === void 0) { frame_duration = 20; }
|
||||
this.handle = 0;
|
||||
this.frame_size = 0;
|
||||
this.in_ptr = 0;
|
||||
this.in_off = 0;
|
||||
this.out_ptr = 0;
|
||||
if (!Opus.validFrameDuration(frame_duration))
|
||||
throw 'invalid frame duration';
|
||||
this.frame_size = sampling_rate * frame_duration / 1000;
|
||||
var err_ptr = allocate(4, 'i32', ALLOC_STACK);
|
||||
this.handle = _opus_encoder_create(sampling_rate, channels, app, err_ptr);
|
||||
if (getValue(err_ptr, 'i32') != 0 /* OK */)
|
||||
throw 'opus_encoder_create failed: ' + getValue(err_ptr, 'i32');
|
||||
this.in_ptr = _malloc(this.frame_size * channels * 4);
|
||||
this.in_len = this.frame_size * channels;
|
||||
this.in_i16 = HEAP16.subarray(this.in_ptr >> 1, (this.in_ptr >> 1) + this.in_len);
|
||||
this.in_f32 = HEAPF32.subarray(this.in_ptr >> 2, (this.in_ptr >> 2) + this.in_len);
|
||||
this.out_bytes = Opus.getMaxFrameSize();
|
||||
this.out_ptr = _malloc(this.out_bytes);
|
||||
this.out_buf = HEAPU8.subarray(this.out_ptr, this.out_ptr + this.out_bytes);
|
||||
}
|
||||
OpusEncoder.prototype.encode = function (pcm) {
|
||||
var output = [];
|
||||
var pcm_off = 0;
|
||||
while (pcm.length - pcm_off >= this.in_len - this.in_off) {
|
||||
if (this.in_off > 0) {
|
||||
this.in_i16.set(pcm.subarray(pcm_off, pcm_off + this.in_len - this.in_off), this.in_off);
|
||||
pcm_off += this.in_len - this.in_off;
|
||||
this.in_off = 0;
|
||||
}
|
||||
else {
|
||||
this.in_i16.set(pcm.subarray(pcm_off, pcm_off + this.in_len));
|
||||
pcm_off += this.in_len;
|
||||
}
|
||||
var ret = _opus_encode(this.handle, this.in_ptr, this.frame_size, this.out_ptr, this.out_bytes);
|
||||
if (ret <= 0)
|
||||
throw 'opus_encode failed: ' + ret;
|
||||
var packet = new ArrayBuffer(ret);
|
||||
new Uint8Array(packet).set(this.out_buf.subarray(0, ret));
|
||||
output.push(packet);
|
||||
}
|
||||
if (pcm_off < pcm.length) {
|
||||
this.in_i16.set(pcm.subarray(pcm_off));
|
||||
this.in_off = pcm.length - pcm_off;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
OpusEncoder.prototype.encode_float = function (pcm) {
|
||||
var output = [];
|
||||
var pcm_off = 0;
|
||||
while (pcm.length - pcm_off >= this.in_len - this.in_off) {
|
||||
if (this.in_off > 0) {
|
||||
this.in_f32.set(pcm.subarray(pcm_off, pcm_off + this.in_len - this.in_off), this.in_off);
|
||||
pcm_off += this.in_len - this.in_off;
|
||||
this.in_off = 0;
|
||||
}
|
||||
else {
|
||||
this.in_f32.set(pcm.subarray(pcm_off, pcm_off + this.in_len));
|
||||
pcm_off += this.in_len;
|
||||
}
|
||||
var ret = _opus_encode_float(this.handle, this.in_ptr, this.frame_size, this.out_ptr, this.out_bytes);
|
||||
if (ret <= 0)
|
||||
throw 'opus_encode failed: ' + ret;
|
||||
var packet = new ArrayBuffer(ret);
|
||||
new Uint8Array(packet).set(this.out_buf.subarray(0, ret));
|
||||
output.push(packet);
|
||||
}
|
||||
if (pcm_off < pcm.length) {
|
||||
this.in_f32.set(pcm.subarray(pcm_off));
|
||||
this.in_off = pcm.length - pcm_off;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
OpusEncoder.prototype.encode_final = function () {
|
||||
if (this.in_off == 0)
|
||||
return new ArrayBuffer(0);
|
||||
for (var i = this.in_off; i < this.in_len; ++i)
|
||||
this.in_i16[i] = 0;
|
||||
var ret = _opus_encode(this.handle, this.in_ptr, this.frame_size, this.out_ptr, this.out_bytes);
|
||||
if (ret <= 0)
|
||||
throw 'opus_encode failed: ' + ret;
|
||||
var packet = new ArrayBuffer(ret);
|
||||
new Uint8Array(packet).set(this.out_buf.subarray(0, ret));
|
||||
return packet;
|
||||
};
|
||||
OpusEncoder.prototype.encode_float_final = function () {
|
||||
if (this.in_off == 0)
|
||||
return new ArrayBuffer(0);
|
||||
for (var i = this.in_off; i < this.in_len; ++i)
|
||||
this.in_f32[i] = 0;
|
||||
var ret = _opus_encode_float(this.handle, this.in_ptr, this.frame_size, this.out_ptr, this.out_bytes);
|
||||
if (ret <= 0)
|
||||
throw 'opus_encode failed: ' + ret;
|
||||
var packet = new ArrayBuffer(ret);
|
||||
new Uint8Array(packet).set(this.out_buf.subarray(0, ret));
|
||||
return packet;
|
||||
};
|
||||
OpusEncoder.prototype.destroy = function () {
|
||||
if (!this.handle)
|
||||
return;
|
||||
_opus_encoder_destroy(this.handle);
|
||||
_free(this.in_ptr);
|
||||
this.handle = this.in_ptr = 0;
|
||||
};
|
||||
return OpusEncoder;
|
||||
})();
|
||||
var OpusDecoder = (function () {
|
||||
function OpusDecoder(sampling_rate, channels) {
|
||||
this.handle = 0;
|
||||
this.in_ptr = 0;
|
||||
this.out_ptr = 0;
|
||||
this.channels = channels;
|
||||
var err_ptr = allocate(4, 'i32', ALLOC_STACK);
|
||||
this.handle = _opus_decoder_create(sampling_rate, channels, err_ptr);
|
||||
if (getValue(err_ptr, 'i32') != 0 /* OK */)
|
||||
throw 'opus_decoder_create failed: ' + getValue(err_ptr, 'i32');
|
||||
this.in_ptr = _malloc(Opus.getMaxFrameSize(channels));
|
||||
this.in_buf = HEAPU8.subarray(this.in_ptr, this.in_ptr + Opus.getMaxFrameSize(channels));
|
||||
this.out_len = Opus.getMaxSamplesPerChannel(sampling_rate);
|
||||
var out_bytes = this.out_len * channels * 4;
|
||||
this.out_ptr = _malloc(out_bytes);
|
||||
this.out_i16 = HEAP16.subarray(this.out_ptr >> 1, (this.out_ptr + out_bytes) >> 1);
|
||||
this.out_f32 = HEAPF32.subarray(this.out_ptr >> 2, (this.out_ptr + out_bytes) >> 2);
|
||||
}
|
||||
OpusDecoder.prototype.decode = function (packet) {
|
||||
this.in_buf.set(new Uint8Array(packet));
|
||||
var ret = _opus_decode(this.handle, this.in_ptr, packet.byteLength, this.out_ptr, this.out_len, 0);
|
||||
if (ret < 0)
|
||||
throw 'opus_decode failed: ' + ret;
|
||||
var samples = new Int16Array(ret * this.channels);
|
||||
samples.set(this.out_i16.subarray(0, samples.length));
|
||||
return samples;
|
||||
};
|
||||
OpusDecoder.prototype.decode_float = function (packet) {
|
||||
this.in_buf.set(new Uint8Array(packet));
|
||||
var ret = _opus_decode_float(this.handle, this.in_ptr, packet.byteLength, this.out_ptr, this.out_len, 0);
|
||||
if (ret < 0)
|
||||
throw 'opus_decode failed: ' + ret;
|
||||
var samples = new Float32Array(ret * this.channels);
|
||||
samples.set(this.out_f32.subarray(0, samples.length));
|
||||
return samples;
|
||||
};
|
||||
OpusDecoder.prototype.destroy = function () {
|
||||
if (!this.handle)
|
||||
return;
|
||||
_opus_decoder_destroy(this.handle);
|
||||
_free(this.in_ptr);
|
||||
_free(this.out_ptr);
|
||||
this.handle = this.in_ptr = this.out_ptr = 0;
|
||||
};
|
||||
return OpusDecoder;
|
||||
})();
|
||||
69
web/js/room.js
vendored
Normal file
69
web/js/room.js
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* Game room module.
|
||||
* @version 1
|
||||
*/
|
||||
const room = (() => {
|
||||
let id = '';
|
||||
|
||||
// UI
|
||||
const roomLabel = $('#room-txt');
|
||||
|
||||
// !to rewrite
|
||||
const parseURLForRoom = () => {
|
||||
let queryDict = {};
|
||||
|
||||
location.search.substr(1)
|
||||
.split('&')
|
||||
.forEach((item) => {
|
||||
queryDict[item.split('=')[0]] = item.split('=')[1]
|
||||
});
|
||||
|
||||
if (typeof queryDict.id === 'string') {
|
||||
return decodeURIComponent(queryDict.id);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
event.sub(GAME_ROOM_AVAILABLE, data => {
|
||||
room.setId(data.roomId);
|
||||
room.save(data.roomId);
|
||||
}, 1);
|
||||
|
||||
return {
|
||||
getId: () => id,
|
||||
setId: (id_) => {
|
||||
id = id_;
|
||||
roomLabel.val(id);
|
||||
},
|
||||
reset: () => {
|
||||
id = '';
|
||||
roomLabel.val(id);
|
||||
},
|
||||
save: (roomIndex) => {
|
||||
localStorage.setItem('roomID', roomIndex);
|
||||
},
|
||||
load: () => localStorage.getItem('roomID'),
|
||||
getLink: () => window.location.href.split('?')[0] + `?id=${encodeURIComponent(room.getId())}`,
|
||||
loadMaybe: () => {
|
||||
// localStorage first
|
||||
//roomID = loadRoomID();
|
||||
|
||||
// Shared URL second
|
||||
const parsedId = parseURLForRoom();
|
||||
if (parsedId !== null) {
|
||||
id = parsedId;
|
||||
}
|
||||
|
||||
return id;
|
||||
},
|
||||
copyToClipboard: () => {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = room.getLink();
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
}
|
||||
})(document, event, location, localStorage, window);
|
||||
124
web/js/utils.js
vendored
124
web/js/utils.js
vendored
|
|
@ -1,124 +0,0 @@
|
|||
function log(msg) {
|
||||
// if (LOG) {
|
||||
// document.getElementById('div').innerHTML += msg + '<br>'
|
||||
// }
|
||||
console.log(msg);
|
||||
}
|
||||
|
||||
function popup(msg) {
|
||||
$("#noti-box").html(msg);
|
||||
$("#noti-box").fadeIn().delay(DEBUG ? 0 : 0).fadeOut();
|
||||
}
|
||||
|
||||
|
||||
function openFullscreen(elem) {
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen();
|
||||
} else if (elem.mozRequestFullScreen) { /* Firefox */
|
||||
elem.mozRequestFullScreen();
|
||||
} else if (elem.webkitRequestFullscreen) { /* Chrome, Safari and Opera */
|
||||
elem.webkitRequestFullscreen();
|
||||
} else if (elem.msRequestFullscreen) { /* IE/Edge */
|
||||
elem.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function closeFullscreen() {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) { /* Firefox */
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.webkitExitFullscreen) { /* Chrome, Safari and Opera */
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.msExitFullscreen) { /* IE/Edge */
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getOS() {
|
||||
// linux? ios?
|
||||
var OSName = "unknown";
|
||||
if (navigator.appVersion.indexOf("Win") !== -1) OSName = "win";
|
||||
else if (navigator.appVersion.indexOf("Mac") !== -1) OSName = "mac";
|
||||
else if (navigator.userAgent.indexOf("Linux") !== -1) OSName = "linux";
|
||||
else if (navigator.userAgent.indexOf("Android") !== -1) OSName = "android";
|
||||
return OSName;
|
||||
}
|
||||
|
||||
|
||||
function getBrowser() {
|
||||
var browserName = "unknown";
|
||||
if (navigator.userAgent.indexOf("Firefox") !== -1) browserName = "firefox";
|
||||
if (navigator.userAgent.indexOf("Chrome") !== -1) browserName = "chrome";
|
||||
if (navigator.userAgent.indexOf("Edge") !== -1) browserName = "edge";
|
||||
if (navigator.userAgent.indexOf("Version/") !== -1) browserName = "safari";
|
||||
if (navigator.userAgent.indexOf("UCBrowser") !== -1) browserName = "uc";
|
||||
return browserName;
|
||||
}
|
||||
|
||||
|
||||
function isPortrait() {
|
||||
// ios / mobile app
|
||||
switch (window.orientation) {
|
||||
case 0:
|
||||
case 180:
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
|
||||
// desktop
|
||||
var orient = screen.msOrientation || screen.mozOrientation || (screen.orientation || {}).type;
|
||||
if (orient === "portrait-primary") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function fixElementLayout(elem, targetWidth, targetHeight) {
|
||||
var width = elem.width();
|
||||
var height = elem.height();
|
||||
|
||||
var st = "translate(-50%, -50%) ";
|
||||
|
||||
// rotate portrait layout
|
||||
if (isPortrait()) {
|
||||
st += `rotate(90deg) `;
|
||||
var tmp = targetHeight;
|
||||
targetHeight = targetWidth;
|
||||
targetWidth = tmp;
|
||||
isLayoutSwitched = true;
|
||||
} else {
|
||||
isLayoutSwitched = false;
|
||||
}
|
||||
|
||||
// scale, fit to target size
|
||||
st += `scale(${Math.min(targetWidth / width, targetHeight / height)}) `;
|
||||
|
||||
elem.css("transform", st);
|
||||
elem.css("-webkit-transform", st);
|
||||
elem.css("-moz-transform", st);
|
||||
}
|
||||
|
||||
|
||||
function loadRoomID() {
|
||||
return localStorage.getItem("roomID");
|
||||
}
|
||||
|
||||
|
||||
function saveRoomID(roomIdx) {
|
||||
localStorage.setItem("roomID", roomIdx);
|
||||
}
|
||||
|
||||
|
||||
function copyToClipboard(str) {
|
||||
const el = document.createElement('textarea');
|
||||
el.value = str;
|
||||
document.body.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(el);
|
||||
};
|
||||
313
web/js/ws.js
vendored
313
web/js/ws.js
vendored
|
|
@ -1,313 +0,0 @@
|
|||
var curPacketID = "";
|
||||
var curSessionID = "";
|
||||
// web socket
|
||||
|
||||
// localStorage first
|
||||
//roomID = loadRoomID();
|
||||
roomID = "";
|
||||
|
||||
// Shared URL second
|
||||
var rid = parseURLForRoom();
|
||||
if (rid !== null) {
|
||||
roomID = rid;
|
||||
}
|
||||
|
||||
// if from URL -> start game immediately!
|
||||
console.log(`ws://${location.host}/ws?room_id=${roomID}`)
|
||||
conn = new WebSocket(`ws://${location.host}/ws?room_id=${roomID}`);
|
||||
|
||||
// Clear old roomID
|
||||
conn.onopen = () => {
|
||||
log("WebSocket is opened. Send ping");
|
||||
log("Send ping pong frequently")
|
||||
pingpongTimer = setInterval(sendPing, 1000 / PINGPONGPS)
|
||||
|
||||
}
|
||||
|
||||
conn.onerror = error => {
|
||||
log(`Websocket error: ${error}`);
|
||||
}
|
||||
|
||||
conn.onclose = () => {
|
||||
log("Websocket closed");
|
||||
}
|
||||
|
||||
conn.onmessage = e => {
|
||||
d = JSON.parse(e.data);
|
||||
switch (d["id"]) {
|
||||
|
||||
case "init":
|
||||
// TODO: Read from struct
|
||||
// init package has 2 part [stunturn, gamelist]
|
||||
// The first element is stunturn address
|
||||
// The rest are list of game
|
||||
data = JSON.parse(d["data"]);
|
||||
stunturn = data[0]
|
||||
startWebRTC(stunturn);
|
||||
data.shift()
|
||||
gameList = [];
|
||||
|
||||
data.forEach(name => {
|
||||
gameList.push(name);
|
||||
});
|
||||
|
||||
log("Received game list");
|
||||
|
||||
// change screen to menu
|
||||
reloadGameMenu();
|
||||
showMenuScreen();
|
||||
|
||||
break;
|
||||
|
||||
case "sdp":
|
||||
log("Got remote sdp");
|
||||
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
|
||||
break;
|
||||
|
||||
case "requestOffer":
|
||||
curPacketID = d["packet_id"];
|
||||
log("Received request offer ", curPacketID)
|
||||
startWebRTC();
|
||||
|
||||
case "heartbeat":
|
||||
// TODO: Calc time
|
||||
break;
|
||||
case "start":
|
||||
roomID = d["room_id"];
|
||||
log(`Got start with room id: ${roomID}`);
|
||||
popup("Started! You can share you game!")
|
||||
saveRoomID(roomID);
|
||||
|
||||
$("#btn-join").html("share");
|
||||
|
||||
// TODO: remove
|
||||
$("#room-txt").val(d["room_id"]);
|
||||
|
||||
break;
|
||||
case "save":
|
||||
log(`Got save response: ${d["data"]}`);
|
||||
popup("Saved");
|
||||
break;
|
||||
case "load":
|
||||
log(`Got load response: ${d["data"]}`);
|
||||
popup("Loaded");
|
||||
break;
|
||||
case "checkLatency":
|
||||
var s = d["data"];
|
||||
var latencyList = [];
|
||||
curPacketID = d["packet_id"];
|
||||
latencyPacketID = curPacketID;
|
||||
addrs = s.split(",")
|
||||
|
||||
var latenciesMap = {};
|
||||
var cntResp = 0;
|
||||
beforeTime = Date.now();
|
||||
for (const addr of addrs) {
|
||||
var sumLatency = 0
|
||||
|
||||
// TODO: Clean code, use async
|
||||
var xmlHttp = new XMLHttpRequest();
|
||||
xmlHttp.open( "GET", "http://"+addr+":9000/echo?_=" + beforeTime, true ); // false for synchronous request, add date to not calling cache
|
||||
xmlHttp.timeout = 2000
|
||||
xmlHttp.ontimeout = () => {
|
||||
cntResp++;
|
||||
afterTime = Date.now();
|
||||
//sumLatency += afterTime - beforeTime
|
||||
latenciesMap[addr] = afterTime - beforeTime
|
||||
if (cntResp == addrs.length) {
|
||||
log(`Send latency list`)
|
||||
console.log(latenciesMap)
|
||||
|
||||
conn.send(JSON.stringify({"id": "checkLatency", "data": JSON.stringify(latenciesMap), "packet_id": latencyPacketID}));
|
||||
//startWebRTC();
|
||||
}
|
||||
}
|
||||
xmlHttp.onload = () => {
|
||||
cntResp++;
|
||||
afterTime = Date.now();
|
||||
//sumLatency += afterTime - beforeTime
|
||||
latenciesMap[addr] = afterTime - beforeTime
|
||||
if (cntResp == addrs.length) {
|
||||
log(`Send latency list ${latenciesMap}`)
|
||||
console.log(latenciesMap)
|
||||
|
||||
//conn.send(JSON.stringify({"id": "checkLatency", "data": latenciesMap, "packet_id": latencyPacketID}));
|
||||
conn.send(JSON.stringify({"id": "checkLatency", "data": JSON.stringify(latenciesMap), "packet_id": latencyPacketID}));
|
||||
//startWebRTC();
|
||||
}
|
||||
}
|
||||
xmlHttp.send( null );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateLatencies(beforeTime, addr, latenciesMap, cntResp, curPacketID) {
|
||||
afterTime = Date.now();
|
||||
//sumLatency += afterTime - beforeTime
|
||||
latenciesMap[addr] = afterTime - beforeTime
|
||||
if (cntResp == addrs.length) {
|
||||
log(`Send latency list ${latenciesMap}`)
|
||||
log(curPacketID)
|
||||
|
||||
conn.send(JSON.stringify({"id": "checkLatency", "data": latenciesMap, "packet_id": curPacketID}));
|
||||
}
|
||||
}
|
||||
|
||||
function sendPing() {
|
||||
// TODO: format the package with time
|
||||
conn.send(JSON.stringify({"id": "heartbeat", "data": Date.now().toString()}));
|
||||
}
|
||||
|
||||
function startWebRTC(iceservers) {
|
||||
log(`received stunturn from worker ${iceservers}`)
|
||||
// webrtc
|
||||
iceservers = JSON.parse(iceservers);
|
||||
pc = new RTCPeerConnection({iceServers: iceservers });
|
||||
|
||||
// input channel, ordered + reliable, id 0
|
||||
inputChannel = pc.createDataChannel('a', {
|
||||
ordered: true,
|
||||
negotiated: true,
|
||||
id: 0,
|
||||
});
|
||||
inputChannel.onopen = () => {
|
||||
log('inputChannel has opened');
|
||||
inputReady = true;
|
||||
// TODO: Event based
|
||||
if (roomID != "") {
|
||||
startGame()
|
||||
}
|
||||
}
|
||||
inputChannel.onclose = () => log('inputChannel has closed');
|
||||
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
log(`iceConnectionState: ${pc.iceConnectionState}`);
|
||||
|
||||
if (pc.iceConnectionState === "connected") {
|
||||
gameReady = true
|
||||
iceSuccess = true
|
||||
if (roomID != "") {
|
||||
startGame()
|
||||
}
|
||||
}
|
||||
else if (pc.iceConnectionState === "failed") {
|
||||
gameReady = false
|
||||
iceSuccess = false
|
||||
log(`failed. Retry...`)
|
||||
pc.createOffer({iceRestart: true }).then(d => {
|
||||
pc.setLocalDescription(d).catch(log);
|
||||
}).catch(log);
|
||||
}
|
||||
else if (pc.iceConnectionState === "disconnected") {
|
||||
stopGameInputTimer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var stream = new MediaStream();
|
||||
document.getElementById("game-screen").srcObject = stream;
|
||||
|
||||
// video channel
|
||||
pc.ontrack = function (event) {
|
||||
stream.addTrack(event.track);
|
||||
}
|
||||
|
||||
|
||||
// candidate packet from STUN
|
||||
pc.onicecandidate = event => {
|
||||
if (event.candidate === null) {
|
||||
// send to ws
|
||||
if (!iceSent) {
|
||||
session = btoa(JSON.stringify(pc.localDescription));
|
||||
log("Send SDP to remote peer");
|
||||
// TODO: Fix curPacketID
|
||||
conn.send(JSON.stringify({"id": "initwebrtc", "data": JSON.stringify({"sdp": session, "is_mobile": isMobileDevice()})}));
|
||||
iceSent = true
|
||||
}
|
||||
} else {
|
||||
console.log(JSON.stringify(event.candidate));
|
||||
// TODO: tidy up, setTimeout multiple time now
|
||||
// timeout
|
||||
setTimeout(() => {
|
||||
if (!iceSent) {
|
||||
log("Ice gathering timeout, send anyway")
|
||||
session = btoa(JSON.stringify(pc.localDescription));
|
||||
conn.send(JSON.stringify({"id": "initwebrtc", "data": JSON.stringify({"sdp": session, "is_mobile": isMobileDevice()})}));
|
||||
iceSent = true;
|
||||
}
|
||||
}, ICE_TIMEOUT)
|
||||
}
|
||||
}
|
||||
|
||||
// receiver only tracks
|
||||
pc.addTransceiver('video', {'direction': 'recvonly'});
|
||||
pc.addTransceiver('audio', {'direction': 'recvonly'});
|
||||
|
||||
// create SDP
|
||||
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).then(d => {
|
||||
log(d.sdp)
|
||||
pc.setLocalDescription(d).catch(log);
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
if (!iceSuccess) {
|
||||
popup("Game cannot load. Please refresh");
|
||||
return false;
|
||||
}
|
||||
// TODO: Add while loop
|
||||
if (!gameReady || !inputReady) {
|
||||
popup("Game is not ready yet. Please wait");
|
||||
return false;
|
||||
}
|
||||
|
||||
var promise = document.getElementById("game-screen").play();
|
||||
if (promise !== undefined) {
|
||||
promise.then(_ => {
|
||||
console.log("Media can autoplay")
|
||||
}).catch(error => {
|
||||
// Usually error happens when we autoplay unmuted video, browser requires manual play.
|
||||
// We already muted video and use separate audio encoding so it's fine now
|
||||
console.log("Media Failed to autoplay")
|
||||
console.log(error)
|
||||
// TODO: Consider workaround
|
||||
});
|
||||
}
|
||||
|
||||
if (screenState != "menu") {
|
||||
return false;
|
||||
}
|
||||
log("Starting game screen");
|
||||
screenState = "game";
|
||||
|
||||
conn.send(JSON.stringify({"id": "start", "data": JSON.stringify({"game_name": gameList[gameIdx], "is_mobile": isMobileDevice()}), "room_id": roomID != null ? roomID : '', "player_index": 1}));
|
||||
|
||||
// clear menu screen
|
||||
stopGameInputTimer();
|
||||
$("#menu-screen").hide()
|
||||
$("#game-screen").show();
|
||||
$("#btn-save").show();
|
||||
$("#btn-load").show();
|
||||
// end clear
|
||||
startGameInputTimer();
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Check mobile type because different mobile can accept different video encoder.
|
||||
function isMobileDevice() {
|
||||
return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf('IEMobile') !== -1);
|
||||
};
|
||||
|
||||
function parseURLForRoom() {
|
||||
var queryDict = {}
|
||||
location.search.substr(1).split("&").forEach(function(item) {
|
||||
queryDict[item.split("=")[0]] = item.split("=")[1]
|
||||
});
|
||||
if (typeof queryDict["id"] === "string") {
|
||||
return decodeURIComponent(queryDict["id"]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue