Merge Tri / Websocket + UI Improve

This commit is contained in:
giongto35 2019-04-07 01:03:03 +08:00
commit 86ffcd7cbe
9 changed files with 498 additions and 46 deletions

View file

@ -11,6 +11,7 @@ RUN apt-get install libvpx-dev -y
RUN go get github.com/pions/webrtc
#RUN go get github.com/gordonklaus/portaudio
RUN go get github.com/gorilla/mux
RUN go get github.com/gorilla/websocket
RUN go install github.com/giongto35/cloud-game
EXPOSE 8000

Binary file not shown.

View file

@ -7,7 +7,27 @@ textarea {
}
</style>
<select id="gameOp">
<option value="Contra.nes">Contra.nes</option>
<option value="Kirby's Adventure.nes">Kirby's Adventure.nes</option>
<option value="Mega Man 2.nes">Mega Man 2.nes</option>
<option value="Metal Gear.nes">Metal Gear.nes</option>
<option value="Mortal Kombat 4.nes">Mortal Kombat 4.nes</option>
<option value="Super Mario Bros 2.nes">Super Mario Bros 2.nes</option>
<option value="Super Mario Bros 3.nes">Super Mario Bros 3.nes</option>
<option value="Super Mario Bros.nes">Super Mario Bros.nes</option>
<option value="Teenage Mutant Ninja Turtles 3.nes">Teenage Mutant Ninja Turtles 3.nes</option>
<option value="VS Super Mario Bros.nes">VS Super Mario Bros.nes</option>
<option value="supermariobros.rom">supermariobros.rom</option>
<option value="zelda.rom">zelda.rom</option>
</select>
<button id="playGame" onclick="window.startSession()" disabled>Play Mario</button>
<br/><br/>
<div id="remoteVideos" ></div> <br />
<h3>Instruction</h3>
<div>
Use Up, Down, Left, Right to Move <br />
Z to jump (A) <br />
@ -15,20 +35,30 @@ textarea {
C is start button <br />
V is select button <br />
Fullscreen media for better gaming experience<br />
</div>
<!--Fullscreen media for better gaming experience<br /-->
</div><br>
<button id="playGame" onclick="window.startSession()" disabled> Play Mario </button>
<div id="div"></div>
<h3>Log:</h3>
<pre id="div"></pre>
<div>
Refresh to retry
🎮<u><i>Refresh to retry when checking is too long</i></u>
</div>
<script>
// miscs
DEBUG = true;
let log = msg => {
if (DEBUG) {
document.getElementById('div').innerHTML += msg + '<br>'
console.log(msg);
}
}
var localSessionDescription = ""
var remoteSessionDescription = ""
// Register with server the session description
function postSession(session) {
if (session == "") {
@ -38,15 +68,14 @@ function postSession(session) {
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
remoteSessionDescription = this.responseText;
document.getElementById('playGame').disabled = false;
document.getElementById('playGame').disabled = false;
}
};
xhttp.open("POST", "/session", true);
xhttp.setRequestHeader("Content-type", "text/plain");
xhttp.send(session);
xhttp.setRequestHeader("Content-type", "application/json");
xhttp.send(JSON.stringify({"game": gameOp.value, "sdp": session}));
}
let pc = new RTCPeerConnection({
iceServers: [
{
@ -54,16 +83,12 @@ let pc = new RTCPeerConnection({
}
]
})
let log = msg => {
document.getElementById('div').innerHTML += msg + '<br>'
}
const dataChannelOptions = {
ordered: false, // do not guarantee order
maxPacketLifeTime: 3000, // in milliseconds
};
let inputChannel = pc.createDataChannel('foo')
inputChannel.onclose = () => console.log('inputChannel has closed')
inputChannel.onopen = () => console.log('inputChannel has opened')
@ -73,7 +98,8 @@ pc.ontrack = function (event) {
var el = document.createElement(event.track.kind)
el.srcObject = event.streams[0]
el.autoplay = true
el.controls = true
// el.controls = true
// el.poster = new URL("https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif");
document.getElementById('remoteVideos').appendChild(el)
}
@ -85,9 +111,7 @@ pc.onicecandidate = event => {
postSession(session)
}
}
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).then(d => pc.setLocalDescription(d)).catch(log)
// Key controller
keyState = {
// controllers
@ -137,13 +161,11 @@ document.body.onkeyup = function(e){
setState(e, false);
};
window.startSession = () => {
let sd = remoteSessionDescription
if (sd === '') {
return alert('Session Description must not be empty')
}
try {
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))));
} catch (e) {
@ -166,7 +188,7 @@ function sendInput() {
ButtonRight
)
*/
if (stateUnchange || unchangePacket > 0) {
st = "";
["a", "b", "select", "start", "up", "down", "left", "right"].forEach(elem => {
@ -205,5 +227,7 @@ pc.oniceconnectionstatechange = e => {
}
</script>
</html>

283
index_ws.html Normal file
View file

@ -0,0 +1,283 @@
<html>
<style>
textarea {
width: 60%;
height: 50px;
}
</style>
<select id="gameOp">
<option value="Contra.nes">Contra.nes</option>
<option value="Kirby's Adventure.nes">Kirby's Adventure.nes</option>
<option value="Mega Man 2.nes">Mega Man 2.nes</option>
<option value="Metal Gear.nes">Metal Gear.nes</option>
<option value="Mortal Kombat 4.nes">Mortal Kombat 4.nes</option>
<option value="Super Mario Bros 2.nes">Super Mario Bros 2.nes</option>
<option value="Super Mario Bros 3.nes">Super Mario Bros 3.nes</option>
<option value="Super Mario Bros.nes">Super Mario Bros.nes</option>
<option value="Teenage Mutant Ninja Turtles 3.nes">Teenage Mutant Ninja Turtles 3.nes</option>
<option value="VS Super Mario Bros.nes">VS Super Mario Bros.nes</option>
<option value="supermariobros.rom">supermariobros.rom</option>
<option value="zelda.rom">zelda.rom</option>
</select>
<button id="play" onclick="window.startGame()">Play</button>
<br/><br/>
<div id="remoteVideos" ></div> <br />
<h3>Instruction</h3>
<div>
Use Up, Down, Left, Right to Move <br />
Z to jump (A) <br />
X to sprint (B) <br />
C is start button <br />
V is select button <br />
<!--Fullscreen media for better gaming experience<br /-->
</div><br>
<h3>Log:</h3>
<pre id="div"></pre>
<div>
🎮<u><i>Refresh to retry when checking is too long</i></u>
</div>
<script>
// miscs
DEBUG = true;
let log = msg => {
if (DEBUG) {
document.getElementById('div').innerHTML += msg + '<br>'
console.log(msg);
}
}
// web socket
window.startGame = () => {
// clear
endInput();
document.getElementById('div').innerHTML = "";
aa = document.getElementsByTagName("video");
for (i = 0; i < aa.length ; ++i) {
aa[i].remove();
}
// end clear
conn = new WebSocket(`ws://${location.host}/ws`);
conn.onopen = () => {
log("WebSocket is opened. Send ping");
conn.send(JSON.stringify({"id": "ping", "data": gameOp.value}));
}
conn.onerror = error => {
log(`Websocket error: ${error}`);
}
conn.onclose = () => {
log("Websocket closed");
// pc.close();
}
conn.onmessage = e => {
d = JSON.parse(e.data);
switch (d["id"]) {
case "pong":
log("Recv pong. Start webrtc");
startWebRTC();
break;
case "sdp":
log("Got remote sdp");
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(d["data"]))));
break;
}
}
// webrtc
pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
// input channel
inputChannel = pc.createDataChannel('foo')
inputChannel.onclose = () => {
log('inputChannel has closed');
}
inputChannel.onopen = () => {
log('inputChannel has opened');
}
inputChannel.onmessage = e => {
log(`Message from DataChannel '${inputChannel.label}' payload '${e.data}'`);
}
pc.oniceconnectionstatechange = e => {
log(`iceConnectionState: ${pc.iceConnectionState}`);
if (pc.iceConnectionState === "connected") {
conn.send(JSON.stringify({"id": "start", "data": ""}));
startInput();
}
else if (pc.iceConnectionState === "disconnected") {
// else { // not sure about this =]
endInput();
}
}
// stream channel
pc.ontrack = function (event) {
var el = document.createElement(event.track.kind);
// console.log(event.streams);
el.srcObject = event.streams[0];
el.autoplay = true;
el.poster = new URL("https://orig00.deviantart.net/cdcd/f/2017/276/a/a/october_2nd___gameboy_poltergeist_by_wanyo-dbpdmnd.gif");
document.getElementById('remoteVideos').appendChild(el)
}
// candidate packet from STUN
pc.onicecandidate = event => {
if (event.candidate === null) {
} else {
console.log(JSON.stringify(event.candidate));
// conn.send(JSON.stringify({"id": "candidate", "data": JSON.stringify(event.candidate)}));
}
}
}
// webrtc
var pc, inputChannel;
var localSessionDescription = "";
var remoteSessionDescription = "";
var conn;
// Input handler
keyState = {
// controllers
a: false,
b: false,
start: false,
select: false,
// navigators
up: false,
down: false,
left: false,
right: false,
}
keyMap = {
37: "left",
38: "up",
39: "right",
40: "down",
90: "a",
88: "b",
67: "start",
86: "select",
}
INPUT_FPS = 100;
INPUT_STATE_PACKET = 5;
stateUnchange = true;
unchangePacket = INPUT_STATE_PACKET;
function setState(e, bo) {
if (e.keyCode in keyMap) {
keyState[keyMap[e.keyCode]] = bo;
stateUnchange = false;
unchangePacket = INPUT_STATE_PACKET;
}
}
document.body.onkeydown = function(e){
setState(e, true);
};
document.body.onkeyup = function(e){
setState(e, false);
};
var timer = null;
function sendInput() {
// prepare key
/*
const (
ButtonA = iota
ButtonB
ButtonSelect
ButtonStart
ButtonUp
ButtonDown
ButtonLeft
ButtonRight
)
*/
if (stateUnchange || unchangePacket > 0) {
st = "";
["a", "b", "select", "start", "up", "down", "left", "right"].forEach(elem => {
st += keyState[elem]?1:0;
});
ss = parseInt(st, 2);
console.log(`Key state string: ${st} ==> ${ss}`);
// send
inputChannel.send(ss);
stateUnchange = false;
unchangePacket--;
}
}
function startInput() {
if (timer == null) {
timer = setInterval(sendInput, 1000 / INPUT_FPS)
}
}
function endInput() {
clearInterval(timer);
timer = null;
}
function startWebRTC() {
// receiver only tracks
pc.addTransceiver('video', {'direction': 'recvonly'});
// create SDP
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: false}).then(d => {
pc.setLocalDescription(d, () => {
// send to ws
session = btoa(JSON.stringify(pc.localDescription));
localSessionDescription = session;
log("Send SDP to remote peer");
conn.send(JSON.stringify({"id": "sdp", "data": session}));
});
}).catch(log);
}
</script>
</html>

148
main.go
View file

@ -1,26 +1,36 @@
package main
import (
pionRTC "github.com/pions/webrtc"
"os"
"fmt"
"image"
"io/ioutil"
"log"
"net/http"
// "time"
"github.com/giongto35/cloud-game/ui"
"github.com/giongto35/cloud-game/util"
"github.com/giongto35/cloud-game/webrtc"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"encoding/json"
)
// var webRTC *webrtc.WebRTC
var width = 256
var height = 240
var gameName = "Contra.nes"
var index string = "./index_http.html"
// var FPS = 60
var upgrader = websocket.Upgrader{}
type WSPacket struct {
ID string `json:"id"`
Data string `json:"data"`
}
func init() {
}
@ -30,23 +40,127 @@ func startGame(path string, imageChannel chan *image.RGBA, inputChannel chan int
}
func main() {
fmt.Printf("Usage: %s [ws]\n", os.Args[0])
fmt.Println("http://localhost:8000")
if len(os.Args) > 1 {
log.Println("Using websocket")
index = "./index_ws.html"
router := mux.NewRouter()
router.HandleFunc("/", getWeb).Methods("GET")
router.HandleFunc("/session", postSession).Methods("POST")
http.ListenAndServe(":8000", router)
// ignore origin
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
http.HandleFunc("/", getWeb)
http.HandleFunc("/ws", ws)
http.ListenAndServe(":8000", nil)
} else {
log.Println("Using http")
router := mux.NewRouter()
router.HandleFunc("/", getWeb).Methods("GET")
router.HandleFunc("/session", postSession).Methods("POST")
http.ListenAndServe(":8000", router)
}
}
func getWeb(w http.ResponseWriter, r *http.Request) {
bs, err := ioutil.ReadFile("./index.html")
bs, err := ioutil.ReadFile(index)
if err != nil {
log.Fatal(err)
}
w.Write(bs)
}
func ws(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Print("upgrade:", err)
return
}
defer c.Close()
log.Println("New Connection")
webRTC := webrtc.NewWebRTC()
// streaming game
// start new games and webrtc stuff?
isDone := false
var gameName string
for !isDone {
mt, message, err := c.ReadMessage()
if err != nil {
log.Println("[!] read:", err)
break
}
req := WSPacket{}
err = json.Unmarshal(message, &req)
if err != nil {
log.Println("[!] json unmarshal:", err)
break
}
// log.Println(req)
// connectivity
res := WSPacket{}
switch req.ID {
case "ping":
gameName = req.Data
log.Println("Ping from server with game:", gameName)
res.ID = "pong"
case "sdp":
log.Println("Received user SDP")
localSession, err := webRTC.StartClient(req.Data, width, height)
if err != nil {
log.Fatalln(err)
}
res.ID = "sdp"
res.Data = localSession
case "candidate":
hi := pionRTC.ICECandidateInit{}
err = json.Unmarshal([]byte(req.Data), &hi)
if err != nil {
log.Println("[!] Cannot parse candidate: ", err)
} else {
// webRTC.AddCandidate(hi)
}
res.ID = "candidate"
case "start":
log.Println("Starting game")
imageChannel := make(chan *image.RGBA, 100)
go screenshotLoop(imageChannel, webRTC)
go startGame("games/" + gameName, imageChannel, webRTC.InputChannel, webRTC)
res.ID = "start"
isDone = true
}
stRes, err := json.Marshal(res)
if err != nil {
log.Println("json marshal:", err)
}
err = c.WriteMessage(mt, []byte(stRes))
if err != nil {
log.Println("write:", err)
break
}
}
}
type SessionPacket struct {
Game string `json:"game"`
SDP string `json:"sdp"`
}
func postSession(w http.ResponseWriter, r *http.Request) {
bs, err := ioutil.ReadAll(r.Body)
if err != nil {
@ -54,16 +168,24 @@ func postSession(w http.ResponseWriter, r *http.Request) {
}
r.Body.Close()
var postPacket SessionPacket
err = json.Unmarshal(bs, &postPacket)
if err != nil {
log.Fatalln(err)
}
log.Println("Got session with game request:", postPacket.Game)
webRTC := webrtc.NewWebRTC()
localSession, err := webRTC.StartClient(string(bs), width, height)
localSession, err := webRTC.StartClient(postPacket.SDP, width, height)
if err != nil {
log.Fatalln(err)
}
imageChannel := make(chan *image.RGBA, 100)
go screenshotLoop(imageChannel, webRTC)
go startGame("games/"+gameName, imageChannel, webRTC.InputChannel, webRTC)
go startGame("games/" + postPacket.Game, imageChannel, webRTC.InputChannel, webRTC)
w.Write([]byte(localSession))
}
@ -81,5 +203,7 @@ func screenshotLoop(imageChannel chan *image.RGBA, webRTC *webrtc.WebRTC) {
yuv := util.RgbaToYuv(image)
webRTC.ImageChannel <- yuv
}
// time.Sleep(10 * time.Millisecond)
// time.Sleep(time.Duration(1000 / FPS) * time.Millisecond)
}
}

View file

@ -1,4 +1,3 @@
// credit to https://github.com/fogleman/nes
package ui
import (
@ -22,11 +21,11 @@ type Director struct {
timestamp float64
imageChannel chan *image.RGBA
inputChannel chan int
webRTC *webrtc.WebRTC
webRTC *webrtc.WebRTC
}
func NewDirector(imageChannel chan *image.RGBA, inputChannel chan int, webRTC *webrtc.WebRTC) *Director {
// func NewDirector(audio *Audio, imageChannel chan *image.RGBA, inputChannel chan int) *Director {
// func NewDirector(audio *Audio, imageChannel chan *image.RGBA, inputChannel chan int) *Director {
director := Director{}
// director.audio = audio
director.imageChannel = imageChannel

View file

@ -24,6 +24,7 @@ type GameView struct {
imageChannel chan *image.RGBA
inputChannel chan int
}
func NewGameView(director *Director, console *nes.Console, title, hash string, imageChannel chan *image.RGBA, inputChannel chan int) View {
@ -85,7 +86,11 @@ func (view *GameView) Update(t, dt float64) {
view.updateControllers()
//fmt.Println(console.Buffer())
console.StepSeconds(dt)
// fps to set frame
view.imageChannel <- console.Buffer()
if view.record {
view.frames = append(view.frames, copyImage(console.Buffer()))
}

View file

@ -1,4 +1,3 @@
// credit to https://github.com/fogleman/nes
package ui
import (

View file

@ -10,6 +10,7 @@ import (
"io/ioutil"
"math/rand"
"strconv"
"time"
vpxEncoder "github.com/giongto35/cloud-game/vpx-encoder"
"github.com/pions/webrtc"
@ -92,8 +93,8 @@ func Decode(in string, obj interface{}) {
// NewWebRTC create
func NewWebRTC() *WebRTC {
w := &WebRTC{
ImageChannel: make(chan []byte, 100),
InputChannel: make(chan int, 100),
ImageChannel: make(chan []byte, 2),
InputChannel: make(chan int, 2),
}
return w
}
@ -103,7 +104,7 @@ type WebRTC struct {
connection *webrtc.PeerConnection
encoder *vpxEncoder.VpxEncoder
isConnected bool
isClosed bool
isClosed bool
// for yuvI420 image
ImageChannel chan []byte
InputChannel chan int
@ -121,7 +122,7 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
// reset client
if w.isConnected {
w.StopClient()
//time.Sleep(2 * time.Second)
time.Sleep(2 * time.Second)
}
encoder, err := vpxEncoder.NewVpxEncoder(width, height, 20, 1200, 5)
@ -146,6 +147,8 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
return "", err
}
// WebRTC state callback
w.connection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String())
if connectionState == webrtc.ICEConnectionStateConnected {
@ -161,8 +164,13 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
}
})
//w.listenInputChannel()
// Data channel
// TODO: take a look at this
w.connection.OnICECandidate(func(iceCandidate *webrtc.ICECandidate) {
fmt.Println(iceCandidate)
})
// Data channel callback
w.connection.OnDataChannel(func(d *webrtc.DataChannel) {
fmt.Printf("New DataChannel %s %d\n", d.Label(), d.ID())
@ -180,22 +188,31 @@ func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, e
})
offer := webrtc.SessionDescription{}
Decode(remoteSession, &offer)
if err != nil {
return "", err
}
err = w.connection.SetRemoteDescription(offer)
if err != nil {
return "", err
}
answer, err := w.connection.CreateAnswer(nil)
if err != nil {
return "", err
}
localSession := Encode(answer)
return localSession, nil
}
// TODO: Take a look at this
func (w *WebRTC) AddCandidate(candidate webrtc.ICECandidateInit) {
err := w.connection.AddICECandidate(candidate)
if err != nil {
fmt.Println("Cannot add candidate: ", err)
}
}
// StopClient disconnect
func (w *WebRTC) StopClient() {
fmt.Println("===StopClient===")
@ -233,7 +250,7 @@ func (w *WebRTC) startStreaming(vp8Track *webrtc.Track) {
// receive frame buffer
go func() {
for i := 0; w.isConnected; i++ {
for w.isConnected {
bs := <-w.encoder.Output
vp8Track.WriteSample(media.Sample{Data: bs, Samples: 1})
}