mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 18:46:11 +00:00
remove candidate, add limit frame, add multi game
This commit is contained in:
parent
81444a405b
commit
1048846266
22 changed files with 1214 additions and 1164 deletions
32
Dockerfile
32
Dockerfile
|
|
@ -1,16 +1,16 @@
|
|||
From golang:1.12
|
||||
|
||||
RUN mkdir -p /go/src/github.com/giongto35/game-online
|
||||
COPY . /go/src/github.com/giongto35/game-online/
|
||||
WORKDIR /go/src/github.com/giongto35/game-online
|
||||
|
||||
# Install server dependencies
|
||||
RUN apt-get update
|
||||
#RUN apt-get install portaudio19-dev -y
|
||||
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 install github.com/giongto35/game-online
|
||||
|
||||
EXPOSE 8000
|
||||
From golang:1.12
|
||||
|
||||
RUN mkdir -p /go/src/github.com/giongto35/game-online
|
||||
COPY . /go/src/github.com/giongto35/game-online/
|
||||
WORKDIR /go/src/github.com/giongto35/game-online
|
||||
|
||||
# Install server dependencies
|
||||
RUN apt-get update
|
||||
#RUN apt-get install portaudio19-dev -y
|
||||
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 install github.com/giongto35/game-online
|
||||
|
||||
EXPOSE 8000
|
||||
|
|
|
|||
BIN
games/1200-in-1.nes
Normal file
BIN
games/1200-in-1.nes
Normal file
Binary file not shown.
BIN
games/Contra.nes
Normal file
BIN
games/Contra.nes
Normal file
Binary file not shown.
BIN
games/Kirby's Adventure.nes
Normal file
BIN
games/Kirby's Adventure.nes
Normal file
Binary file not shown.
BIN
games/Mega Man 2.nes
Normal file
BIN
games/Mega Man 2.nes
Normal file
Binary file not shown.
BIN
games/Mega Man.nes
Normal file
BIN
games/Mega Man.nes
Normal file
Binary file not shown.
BIN
games/Metal Gear.nes
Normal file
BIN
games/Metal Gear.nes
Normal file
Binary file not shown.
BIN
games/Mike Tyson.nes
Normal file
BIN
games/Mike Tyson.nes
Normal file
Binary file not shown.
BIN
games/Mortal Kombat 4.nes
Normal file
BIN
games/Mortal Kombat 4.nes
Normal file
Binary file not shown.
BIN
games/Super Mario Bros 2.nes
Normal file
BIN
games/Super Mario Bros 2.nes
Normal file
Binary file not shown.
BIN
games/Super Mario Bros 3.nes
Normal file
BIN
games/Super Mario Bros 3.nes
Normal file
Binary file not shown.
BIN
games/Super Mario Bros.nes
Normal file
BIN
games/Super Mario Bros.nes
Normal file
Binary file not shown.
BIN
games/Teenage Mutant Ninja Turtles 3.nes
Normal file
BIN
games/Teenage Mutant Ninja Turtles 3.nes
Normal file
Binary file not shown.
BIN
games/VS Super Mario Bros.nes
Normal file
BIN
games/VS Super Mario Bros.nes
Normal file
Binary file not shown.
565
index.html
565
index.html
|
|
@ -1,266 +1,299 @@
|
|||
<html>
|
||||
|
||||
<style>
|
||||
textarea {
|
||||
width: 60%;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="remoteVideos" ></div> <br />
|
||||
<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>
|
||||
|
||||
<button id="playGame" onclick="window.startSession()" disabled> Play Mario </button>
|
||||
<div id="div"></div>
|
||||
|
||||
<div>
|
||||
Refresh to retry
|
||||
</div>
|
||||
<script>
|
||||
|
||||
// miscs
|
||||
DEBUG = true;
|
||||
|
||||
let log = msg => {
|
||||
if (DEBUG) {
|
||||
document.getElementById('div').innerHTML += msg + '<br>'
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
let pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
|
||||
|
||||
var localSessionDescription = ""
|
||||
var remoteSessionDescription = ""
|
||||
|
||||
|
||||
var conn = new WebSocket(`ws://${location.host}/ws`);
|
||||
conn.onopen = () => {
|
||||
log("WebSocket is opened. Send ping");
|
||||
conn.send(JSON.stringify({"id": "ping", "data": ""}));
|
||||
}
|
||||
|
||||
conn.onerror = error => {
|
||||
log(`Websocket error: ${error}`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
window.startSession = () => {
|
||||
let sd = remoteSessionDescription
|
||||
if (sd === '') {
|
||||
return alert('Session Description must not be empty')
|
||||
}
|
||||
|
||||
try {
|
||||
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))));
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}
|
||||
|
||||
function postSession(session) {
|
||||
if (session == "") {
|
||||
return;
|
||||
}
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
remoteSessionDescription = this.responseText;
|
||||
document.getElementById('playGame').disabled = false;
|
||||
}
|
||||
};
|
||||
xhttp.open("POST", "/session", true);
|
||||
xhttp.setRequestHeader("Content-type", "text/plain");
|
||||
xhttp.send(session);
|
||||
|
||||
}
|
||||
|
||||
|
||||
// input channel
|
||||
let 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}'`);
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
log(`iceConnectionState: ${pc.iceConnectionState}`);
|
||||
|
||||
if (pc.iceConnectionState === "connected") {
|
||||
conn.send(JSON.stringify({"id": "start", "data": ""}));
|
||||
startInput();
|
||||
}
|
||||
else if (pc.iceConnectionState === "disconnected") {
|
||||
endInput();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// stream channel
|
||||
pc.ontrack = function (event) {
|
||||
var el = document.createElement(event.track.kind)
|
||||
el.srcObject = event.streams[0]
|
||||
el.autoplay = true
|
||||
el.controls = true
|
||||
|
||||
document.getElementById('remoteVideos').appendChild(el)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// candidate packet from STUN
|
||||
pc.onicecandidate = event => {
|
||||
if (event.candidate === null) {
|
||||
// var session = btoa(JSON.stringify(pc.localDescription));
|
||||
// localSessionDescription = session;
|
||||
// postSession(session)
|
||||
} else {
|
||||
console.log(JSON.stringify(event.candidate));
|
||||
conn.send(JSON.stringify({"id": "candidate", "data": JSON.stringify(event.candidate)}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function startWebRTC() {
|
||||
// create SDP
|
||||
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).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>
|
||||
<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="Mega Man.nes">Mega Man.nes</option>
|
||||
<option value="Metal Gear.nes">Metal Gear.nes</option>
|
||||
<option value="Mike Tyson.nes">Mike Tyson.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-->
|
||||
<button id="play" onclick="window.startGame()">Play</button>
|
||||
|
||||
<br/>
|
||||
|
||||
<div id="remoteVideos" ></div> <br />
|
||||
<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>
|
||||
|
||||
|
||||
<div id="div"></div>
|
||||
|
||||
<div>
|
||||
Refresh to retry
|
||||
</div>
|
||||
<script>
|
||||
|
||||
// miscs
|
||||
DEBUG = true;
|
||||
|
||||
let log = msg => {
|
||||
if (DEBUG) {
|
||||
document.getElementById('div').innerHTML += msg + '<br>'
|
||||
console.log(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// for http server
|
||||
window.startSession = () => {
|
||||
let sd = remoteSessionDescription
|
||||
if (sd === '') {
|
||||
return alert('Session Description must not be empty')
|
||||
}
|
||||
|
||||
try {
|
||||
pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(atob(sd))));
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}
|
||||
|
||||
function postSession(session) {
|
||||
if (session == "") {
|
||||
return;
|
||||
}
|
||||
var xhttp = new XMLHttpRequest();
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
remoteSessionDescription = this.responseText;
|
||||
document.getElementById('playGame').disabled = false;
|
||||
}
|
||||
};
|
||||
xhttp.open("POST", "/session", true);
|
||||
xhttp.setRequestHeader("Content-type", "text/plain");
|
||||
xhttp.send(session);
|
||||
|
||||
}
|
||||
|
||||
|
||||
// web socket
|
||||
|
||||
window.startGame = () => {
|
||||
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
|
||||
let pc = new RTCPeerConnection({iceServers: [{urls: 'stun:stun.l.google.com:19302'}]})
|
||||
|
||||
var localSessionDescription = ""
|
||||
var remoteSessionDescription = ""
|
||||
var conn;
|
||||
|
||||
|
||||
// input channel
|
||||
let 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}'`);
|
||||
}
|
||||
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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)
|
||||
el.srcObject = event.streams[0]
|
||||
el.autoplay = true
|
||||
el.controls = true
|
||||
|
||||
document.getElementById('remoteVideos').appendChild(el)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// candidate packet from STUN
|
||||
pc.onicecandidate = event => {
|
||||
if (event.candidate === null) {
|
||||
// var session = btoa(JSON.stringify(pc.localDescription));
|
||||
// localSessionDescription = session;
|
||||
// postSession(session)
|
||||
} else {
|
||||
console.log(JSON.stringify(event.candidate));
|
||||
// conn.send(JSON.stringify({"id": "candidate", "data": JSON.stringify(event.candidate)}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function startWebRTC() {
|
||||
// create SDP
|
||||
pc.createOffer({offerToReceiveVideo: true, offerToReceiveAudio: true}).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>
|
||||
|
|
|
|||
370
main.go
370
main.go
|
|
@ -1,182 +1,188 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
pionRTC "github.com/pions/webrtc"
|
||||
"fmt"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
// "time"
|
||||
|
||||
"github.com/giongto35/game-online/ui"
|
||||
"github.com/giongto35/game-online/util"
|
||||
"github.com/giongto35/game-online/webrtc"
|
||||
|
||||
// "github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// var webRTC *webrtc.WebRTC
|
||||
var width = 256
|
||||
var height = 240
|
||||
var gameName = "supermariobros.rom"
|
||||
|
||||
// var FPS = 60
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
type WSPacket struct {
|
||||
ID string `json:"id"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func startGame(path string, imageChannel chan *image.RGBA, inputChannel chan int, webRTC *webrtc.WebRTC) {
|
||||
ui.Run([]string{path}, imageChannel, inputChannel, webRTC)
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("http://localhost:8000")
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func getWeb(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := ioutil.ReadFile("./index.html")
|
||||
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
|
||||
|
||||
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":
|
||||
res.ID = "pong"
|
||||
|
||||
case "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 {
|
||||
fmt.Println("[!] Cannot parse candidate: ", err)
|
||||
} else {
|
||||
webRTC.AddCandidate(hi)
|
||||
}
|
||||
res.ID = "candidate"
|
||||
|
||||
case "start":
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func postSession(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
webRTC := webrtc.NewWebRTC()
|
||||
|
||||
localSession, err := webRTC.StartClient(string(bs), 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)
|
||||
|
||||
w.Write([]byte(localSession))
|
||||
}
|
||||
|
||||
// func screenshotLoop(imageChannel chan *image.RGBA) {
|
||||
func screenshotLoop(imageChannel chan *image.RGBA, webRTC *webrtc.WebRTC) {
|
||||
for image := range imageChannel {
|
||||
// Client stopped
|
||||
if webRTC.IsClosed() {
|
||||
break
|
||||
}
|
||||
|
||||
// encode frame
|
||||
if webRTC.IsConnected() {
|
||||
yuv := util.RgbaToYuv(image)
|
||||
webRTC.ImageChannel <- yuv
|
||||
}
|
||||
}
|
||||
}
|
||||
package main
|
||||
|
||||
import (
|
||||
pionRTC "github.com/pions/webrtc"
|
||||
"fmt"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/game-online/ui"
|
||||
"github.com/giongto35/game-online/util"
|
||||
"github.com/giongto35/game-online/webrtc"
|
||||
|
||||
// "github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// var webRTC *webrtc.WebRTC
|
||||
var width = 256
|
||||
var height = 240
|
||||
// var gameName = "supermariobros.rom"
|
||||
var gameName string
|
||||
|
||||
// var FPS = 60
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
type WSPacket struct {
|
||||
ID string `json:"id"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
}
|
||||
|
||||
func startGame(path string, imageChannel chan *image.RGBA, inputChannel chan int, webRTC *webrtc.WebRTC) {
|
||||
ui.Run([]string{path}, imageChannel, inputChannel, webRTC)
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Println("http://localhost:8000")
|
||||
fmt.Println(time.Now().UnixNano())
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
func getWeb(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := ioutil.ReadFile("./index.html")
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func postSession(w http.ResponseWriter, r *http.Request) {
|
||||
bs, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
r.Body.Close()
|
||||
|
||||
webRTC := webrtc.NewWebRTC()
|
||||
|
||||
localSession, err := webRTC.StartClient(string(bs), 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)
|
||||
|
||||
w.Write([]byte(localSession))
|
||||
}
|
||||
|
||||
// func screenshotLoop(imageChannel chan *image.RGBA) {
|
||||
func screenshotLoop(imageChannel chan *image.RGBA, webRTC *webrtc.WebRTC) {
|
||||
for image := range imageChannel {
|
||||
// Client stopped
|
||||
if webRTC.IsClosed() {
|
||||
break
|
||||
}
|
||||
|
||||
// encode frame
|
||||
if webRTC.IsConnected() {
|
||||
yuv := util.RgbaToYuv(image)
|
||||
webRTC.ImageChannel <- yuv
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
174
ui/director.go
174
ui/director.go
|
|
@ -1,87 +1,87 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/game-online/nes"
|
||||
"github.com/giongto35/game-online/webrtc"
|
||||
)
|
||||
|
||||
type View interface {
|
||||
Enter()
|
||||
Exit()
|
||||
Update(t, dt float64)
|
||||
}
|
||||
|
||||
type Director struct {
|
||||
// audio *Audio
|
||||
view View
|
||||
timestamp float64
|
||||
imageChannel chan *image.RGBA
|
||||
inputChannel chan int
|
||||
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 {
|
||||
director := Director{}
|
||||
// director.audio = audio
|
||||
director.imageChannel = imageChannel
|
||||
director.inputChannel = inputChannel
|
||||
director.webRTC = webRTC
|
||||
return &director
|
||||
}
|
||||
|
||||
func (d *Director) SetView(view View) {
|
||||
if d.view != nil {
|
||||
d.view.Exit()
|
||||
}
|
||||
d.view = view
|
||||
if d.view != nil {
|
||||
d.view.Enter()
|
||||
}
|
||||
d.timestamp = float64(time.Now().Nanosecond()) / float64(time.Second)
|
||||
}
|
||||
|
||||
func (d *Director) Step() {
|
||||
//gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
timestamp := float64(time.Now().Nanosecond()) / float64(time.Second)
|
||||
dt := timestamp - d.timestamp
|
||||
d.timestamp = timestamp
|
||||
if d.view != nil {
|
||||
d.view.Update(timestamp, dt)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Director) Start(paths []string) {
|
||||
if len(paths) == 1 {
|
||||
d.PlayGame(paths[0])
|
||||
}
|
||||
d.Run()
|
||||
}
|
||||
|
||||
func (d *Director) Run() {
|
||||
for {
|
||||
// quit game
|
||||
if d.webRTC.IsClosed() {
|
||||
break
|
||||
}
|
||||
|
||||
d.Step()
|
||||
}
|
||||
d.SetView(nil)
|
||||
}
|
||||
|
||||
func (d *Director) PlayGame(path string) {
|
||||
hash, err := hashFile(path)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
console, err := nes.NewConsole(path)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
d.SetView(NewGameView(d, console, path, hash, d.imageChannel, d.inputChannel))
|
||||
}
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/game-online/nes"
|
||||
"github.com/giongto35/game-online/webrtc"
|
||||
)
|
||||
|
||||
type View interface {
|
||||
Enter()
|
||||
Exit()
|
||||
Update(t, dt float64)
|
||||
}
|
||||
|
||||
type Director struct {
|
||||
// audio *Audio
|
||||
view View
|
||||
timestamp float64
|
||||
imageChannel chan *image.RGBA
|
||||
inputChannel chan int
|
||||
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 {
|
||||
director := Director{}
|
||||
// director.audio = audio
|
||||
director.imageChannel = imageChannel
|
||||
director.inputChannel = inputChannel
|
||||
director.webRTC = webRTC
|
||||
return &director
|
||||
}
|
||||
|
||||
func (d *Director) SetView(view View) {
|
||||
if d.view != nil {
|
||||
d.view.Exit()
|
||||
}
|
||||
d.view = view
|
||||
if d.view != nil {
|
||||
d.view.Enter()
|
||||
}
|
||||
d.timestamp = float64(time.Now().Nanosecond()) / float64(time.Second)
|
||||
}
|
||||
|
||||
func (d *Director) Step() {
|
||||
//gl.Clear(gl.COLOR_BUFFER_BIT)
|
||||
timestamp := float64(time.Now().Nanosecond()) / float64(time.Second)
|
||||
dt := timestamp - d.timestamp
|
||||
d.timestamp = timestamp
|
||||
if d.view != nil {
|
||||
d.view.Update(timestamp, dt)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Director) Start(paths []string) {
|
||||
if len(paths) == 1 {
|
||||
d.PlayGame(paths[0])
|
||||
}
|
||||
d.Run()
|
||||
}
|
||||
|
||||
func (d *Director) Run() {
|
||||
for {
|
||||
// quit game
|
||||
if d.webRTC.IsClosed() {
|
||||
break
|
||||
}
|
||||
|
||||
d.Step()
|
||||
}
|
||||
d.SetView(nil)
|
||||
}
|
||||
|
||||
func (d *Director) PlayGame(path string) {
|
||||
hash, err := hashFile(path)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
console, err := nes.NewConsole(path)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
d.SetView(NewGameView(d, console, path, hash, d.imageChannel, d.inputChannel))
|
||||
}
|
||||
|
|
|
|||
223
ui/gameview.go
223
ui/gameview.go
|
|
@ -1,106 +1,117 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"fmt"
|
||||
// "strconv"
|
||||
|
||||
"github.com/giongto35/game-online/nes"
|
||||
)
|
||||
|
||||
const padding = 0
|
||||
|
||||
type GameView struct {
|
||||
director *Director
|
||||
console *nes.Console
|
||||
title string
|
||||
hash string
|
||||
record bool
|
||||
frames []image.Image
|
||||
|
||||
keyPressed [8]bool
|
||||
|
||||
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 {
|
||||
gameview := &GameView{director, console, title, hash, false, nil, [8]bool{false}, imageChannel, inputChannel}
|
||||
go gameview.ListenToInputChannel()
|
||||
return gameview
|
||||
}
|
||||
|
||||
func (view *GameView) ListenToInputChannel() {
|
||||
for {
|
||||
key := <-view.inputChannel
|
||||
s := fmt.Sprintf("%.8b", key)
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '1' {
|
||||
view.keyPressed[i] = true
|
||||
} else {
|
||||
view.keyPressed[i] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *GameView) Enter() {
|
||||
// view.console.SetAudioChannel(view.director.audio.channel)
|
||||
// view.console.SetAudioSampleRate(view.director.audio.sampleRate)
|
||||
// load state
|
||||
if err := view.console.LoadState(savePath(view.hash)); err == nil {
|
||||
return
|
||||
} else {
|
||||
view.console.Reset()
|
||||
}
|
||||
// load sram
|
||||
cartridge := view.console.Cartridge
|
||||
if cartridge.Battery != 0 {
|
||||
if sram, err := readSRAM(sramPath(view.hash)); err == nil {
|
||||
cartridge.SRAM = sram
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *GameView) Exit() {
|
||||
// view.console.SetAudioChannel(nil)
|
||||
// view.console.SetAudioSampleRate(0)
|
||||
// save sram
|
||||
cartridge := view.console.Cartridge
|
||||
if cartridge.Battery != 0 {
|
||||
writeSRAM(sramPath(view.hash), cartridge.SRAM)
|
||||
}
|
||||
// save state
|
||||
view.console.SaveState(savePath(view.hash))
|
||||
}
|
||||
|
||||
func (view *GameView) Update(t, dt float64) {
|
||||
if dt > 1 {
|
||||
dt = 0
|
||||
}
|
||||
console := view.console
|
||||
//updateControllers(window, console)
|
||||
view.updateControllers()
|
||||
//fmt.Println(console.Buffer())
|
||||
console.StepSeconds(dt)
|
||||
view.imageChannel <- console.Buffer()
|
||||
if view.record {
|
||||
view.frames = append(view.frames, copyImage(console.Buffer()))
|
||||
}
|
||||
}
|
||||
|
||||
func (view *GameView) updateControllers() {
|
||||
// TODO: switch case
|
||||
// var buttons [8]bool
|
||||
// buttons[nes.ButtonLeft] = view.keyPressed[37]
|
||||
// buttons[nes.ButtonUp] = view.keyPressed[38]
|
||||
// buttons[nes.ButtonRight] = view.keyPressed[39]
|
||||
// buttons[nes.ButtonDown] = view.keyPressed[40]
|
||||
// buttons[nes.ButtonA] = view.keyPressed[32]
|
||||
// buttons[nes.ButtonB] = view.keyPressed[17]
|
||||
// buttons[nes.ButtonStart] = view.keyPressed[13]
|
||||
// buttons[nes.ButtonSelect] = view.keyPressed[16]
|
||||
// view.console.Controller1.SetButtons(buttons)
|
||||
view.console.Controller1.SetButtons(view.keyPressed)
|
||||
}
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/giongto35/game-online/nes"
|
||||
)
|
||||
|
||||
const padding = 0
|
||||
|
||||
type GameView struct {
|
||||
director *Director
|
||||
console *nes.Console
|
||||
title string
|
||||
hash string
|
||||
record bool
|
||||
frames []image.Image
|
||||
|
||||
keyPressed [8]bool
|
||||
|
||||
imageChannel chan *image.RGBA
|
||||
inputChannel chan int
|
||||
|
||||
nanotime int64
|
||||
}
|
||||
|
||||
func NewGameView(director *Director, console *nes.Console, title, hash string, imageChannel chan *image.RGBA, inputChannel chan int) View {
|
||||
gameview := &GameView{director, console, title, hash, false, nil, [8]bool{false}, imageChannel, inputChannel, time.Now().UnixNano()}
|
||||
go gameview.ListenToInputChannel()
|
||||
return gameview
|
||||
}
|
||||
|
||||
func (view *GameView) ListenToInputChannel() {
|
||||
for {
|
||||
key := <-view.inputChannel
|
||||
s := fmt.Sprintf("%.8b", key)
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] == '1' {
|
||||
view.keyPressed[i] = true
|
||||
} else {
|
||||
view.keyPressed[i] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *GameView) Enter() {
|
||||
// view.console.SetAudioChannel(view.director.audio.channel)
|
||||
// view.console.SetAudioSampleRate(view.director.audio.sampleRate)
|
||||
// load state
|
||||
if err := view.console.LoadState(savePath(view.hash)); err == nil {
|
||||
return
|
||||
} else {
|
||||
view.console.Reset()
|
||||
}
|
||||
// load sram
|
||||
cartridge := view.console.Cartridge
|
||||
if cartridge.Battery != 0 {
|
||||
if sram, err := readSRAM(sramPath(view.hash)); err == nil {
|
||||
cartridge.SRAM = sram
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (view *GameView) Exit() {
|
||||
// view.console.SetAudioChannel(nil)
|
||||
// view.console.SetAudioSampleRate(0)
|
||||
// save sram
|
||||
cartridge := view.console.Cartridge
|
||||
if cartridge.Battery != 0 {
|
||||
writeSRAM(sramPath(view.hash), cartridge.SRAM)
|
||||
}
|
||||
// save state
|
||||
view.console.SaveState(savePath(view.hash))
|
||||
}
|
||||
|
||||
func (view *GameView) Update(t, dt float64) {
|
||||
if dt > 1 {
|
||||
dt = 0
|
||||
}
|
||||
console := view.console
|
||||
//updateControllers(window, console)
|
||||
view.updateControllers()
|
||||
//fmt.Println(console.Buffer())
|
||||
console.StepSeconds(dt)
|
||||
|
||||
// fps to set frame
|
||||
n := time.Now().UnixNano()
|
||||
if n - view.nanotime > 1000000000 / 100000 {
|
||||
view.nanotime = n
|
||||
view.imageChannel <- console.Buffer()
|
||||
}
|
||||
|
||||
|
||||
|
||||
if view.record {
|
||||
view.frames = append(view.frames, copyImage(console.Buffer()))
|
||||
}
|
||||
}
|
||||
|
||||
func (view *GameView) updateControllers() {
|
||||
// TODO: switch case
|
||||
// var buttons [8]bool
|
||||
// buttons[nes.ButtonLeft] = view.keyPressed[37]
|
||||
// buttons[nes.ButtonUp] = view.keyPressed[38]
|
||||
// buttons[nes.ButtonRight] = view.keyPressed[39]
|
||||
// buttons[nes.ButtonDown] = view.keyPressed[40]
|
||||
// buttons[nes.ButtonA] = view.keyPressed[32]
|
||||
// buttons[nes.ButtonB] = view.keyPressed[17]
|
||||
// buttons[nes.ButtonStart] = view.keyPressed[13]
|
||||
// buttons[nes.ButtonSelect] = view.keyPressed[16]
|
||||
// view.console.Controller1.SetButtons(buttons)
|
||||
view.console.Controller1.SetButtons(view.keyPressed)
|
||||
}
|
||||
|
|
|
|||
84
ui/run.go
84
ui/run.go
|
|
@ -1,42 +1,42 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
// "log"
|
||||
"runtime"
|
||||
|
||||
// "github.com/gordonklaus/portaudio"
|
||||
"github.com/giongto35/game-online/webrtc"
|
||||
)
|
||||
|
||||
const (
|
||||
width = 256
|
||||
height = 240
|
||||
scale = 3
|
||||
title = "NES"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// we need a parallel OS thread to avoid audio stuttering
|
||||
runtime.GOMAXPROCS(2)
|
||||
|
||||
// we need to keep OpenGL calls on a single thread
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func Run(paths []string, imageChannel chan *image.RGBA, inputChannel chan int, webRTC *webrtc.WebRTC) {
|
||||
// initialize audio
|
||||
// portaudio.Initialize()
|
||||
// defer portaudio.Terminate()
|
||||
|
||||
// audio := NewAudio()
|
||||
// if err := audio.Start(); err != nil {
|
||||
// log.Fatalln(err)
|
||||
// }
|
||||
// defer audio.Stop()
|
||||
|
||||
// run director
|
||||
director := NewDirector(imageChannel, inputChannel, webRTC)
|
||||
// director := NewDirector(audio, imageChannel, inputChannel)
|
||||
director.Start(paths)
|
||||
}
|
||||
package ui
|
||||
|
||||
import (
|
||||
"image"
|
||||
// "log"
|
||||
"runtime"
|
||||
|
||||
// "github.com/gordonklaus/portaudio"
|
||||
"github.com/giongto35/game-online/webrtc"
|
||||
)
|
||||
|
||||
const (
|
||||
width = 256
|
||||
height = 240
|
||||
scale = 3
|
||||
title = "NES"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// we need a parallel OS thread to avoid audio stuttering
|
||||
// runtime.GOMAXPROCS(2)
|
||||
|
||||
// we need to keep OpenGL calls on a single thread
|
||||
runtime.LockOSThread()
|
||||
}
|
||||
|
||||
func Run(paths []string, imageChannel chan *image.RGBA, inputChannel chan int, webRTC *webrtc.WebRTC) {
|
||||
// initialize audio
|
||||
// portaudio.Initialize()
|
||||
// defer portaudio.Terminate()
|
||||
|
||||
// audio := NewAudio()
|
||||
// if err := audio.Start(); err != nil {
|
||||
// log.Fatalln(err)
|
||||
// }
|
||||
// defer audio.Stop()
|
||||
|
||||
// run director
|
||||
director := NewDirector(imageChannel, inputChannel, webRTC)
|
||||
// director := NewDirector(audio, imageChannel, inputChannel)
|
||||
director.Start(paths)
|
||||
}
|
||||
|
|
|
|||
306
ui/util.go
306
ui/util.go
|
|
@ -1,153 +1,153 @@
|
|||
package ui
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
|
||||
"github.com/giongto35/game-online/nes"
|
||||
)
|
||||
|
||||
var homeDir string
|
||||
|
||||
func init() {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
homeDir = u.HomeDir
|
||||
}
|
||||
|
||||
func thumbnailURL(hash string) string {
|
||||
return "http://www.michaelfogleman.com/static/nes/" + hash + ".png"
|
||||
}
|
||||
|
||||
func thumbnailPath(hash string) string {
|
||||
return homeDir + "/.nes/thumbnail/" + hash + ".png"
|
||||
}
|
||||
|
||||
func sramPath(hash string) string {
|
||||
return homeDir + "/.nes/sram/" + hash + ".dat"
|
||||
}
|
||||
|
||||
func savePath(hash string) string {
|
||||
return homeDir + "/.nes/save/" + hash + ".dat"
|
||||
}
|
||||
|
||||
func combineButtons(a, b [8]bool) [8]bool {
|
||||
var result [8]bool
|
||||
for i := 0; i < 8; i++ {
|
||||
result[i] = a[i] || b[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func hashFile(path string) (string, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", md5.Sum(data)), nil
|
||||
}
|
||||
func copyImage(src image.Image) *image.RGBA {
|
||||
dst := image.NewRGBA(src.Bounds())
|
||||
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
|
||||
return dst
|
||||
}
|
||||
|
||||
func loadPNG(path string) (image.Image, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
return png.Decode(file)
|
||||
}
|
||||
|
||||
func savePNG(path string, im image.Image) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return png.Encode(file, im)
|
||||
}
|
||||
|
||||
func saveGIF(path string, frames []image.Image) error {
|
||||
var palette []color.Color
|
||||
for _, c := range nes.Palette {
|
||||
palette = append(palette, c)
|
||||
}
|
||||
g := gif.GIF{}
|
||||
for i, src := range frames {
|
||||
if i%3 != 0 {
|
||||
continue
|
||||
}
|
||||
dst := image.NewPaletted(src.Bounds(), palette)
|
||||
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
|
||||
g.Image = append(g.Image, dst)
|
||||
g.Delay = append(g.Delay, 5)
|
||||
}
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return gif.EncodeAll(file, &g)
|
||||
}
|
||||
|
||||
func screenshot(im image.Image) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
path := fmt.Sprintf("%03d.png", i)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
savePNG(path, im)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animation(frames []image.Image) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
path := fmt.Sprintf("%03d.gif", i)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
saveGIF(path, frames)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeSRAM(filename string, sram []byte) error {
|
||||
dir, _ := path.Split(filename)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return binary.Write(file, binary.LittleEndian, sram)
|
||||
}
|
||||
|
||||
func readSRAM(filename string) ([]byte, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
sram := make([]byte, 0x2000)
|
||||
if err := binary.Read(file, binary.LittleEndian, sram); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sram, nil
|
||||
}
|
||||
package ui
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
|
||||
"github.com/giongto35/game-online/nes"
|
||||
)
|
||||
|
||||
var homeDir string
|
||||
|
||||
func init() {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
homeDir = u.HomeDir
|
||||
}
|
||||
|
||||
func thumbnailURL(hash string) string {
|
||||
return "http://www.michaelfogleman.com/static/nes/" + hash + ".png"
|
||||
}
|
||||
|
||||
func thumbnailPath(hash string) string {
|
||||
return homeDir + "/.nes/thumbnail/" + hash + ".png"
|
||||
}
|
||||
|
||||
func sramPath(hash string) string {
|
||||
return homeDir + "/.nes/sram/" + hash + ".dat"
|
||||
}
|
||||
|
||||
func savePath(hash string) string {
|
||||
return homeDir + "/.nes/save/" + hash + ".dat"
|
||||
}
|
||||
|
||||
func combineButtons(a, b [8]bool) [8]bool {
|
||||
var result [8]bool
|
||||
for i := 0; i < 8; i++ {
|
||||
result[i] = a[i] || b[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func hashFile(path string) (string, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%x", md5.Sum(data)), nil
|
||||
}
|
||||
func copyImage(src image.Image) *image.RGBA {
|
||||
dst := image.NewRGBA(src.Bounds())
|
||||
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
|
||||
return dst
|
||||
}
|
||||
|
||||
func loadPNG(path string) (image.Image, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
return png.Decode(file)
|
||||
}
|
||||
|
||||
func savePNG(path string, im image.Image) error {
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return png.Encode(file, im)
|
||||
}
|
||||
|
||||
func saveGIF(path string, frames []image.Image) error {
|
||||
var palette []color.Color
|
||||
for _, c := range nes.Palette {
|
||||
palette = append(palette, c)
|
||||
}
|
||||
g := gif.GIF{}
|
||||
for i, src := range frames {
|
||||
if i%3 != 0 {
|
||||
continue
|
||||
}
|
||||
dst := image.NewPaletted(src.Bounds(), palette)
|
||||
draw.Draw(dst, dst.Rect, src, image.ZP, draw.Src)
|
||||
g.Image = append(g.Image, dst)
|
||||
g.Delay = append(g.Delay, 5)
|
||||
}
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return gif.EncodeAll(file, &g)
|
||||
}
|
||||
|
||||
func screenshot(im image.Image) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
path := fmt.Sprintf("%03d.png", i)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
savePNG(path, im)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animation(frames []image.Image) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
path := fmt.Sprintf("%03d.gif", i)
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
saveGIF(path, frames)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeSRAM(filename string, sram []byte) error {
|
||||
dir, _ := path.Split(filename)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return binary.Write(file, binary.LittleEndian, sram)
|
||||
}
|
||||
|
||||
func readSRAM(filename string) ([]byte, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
sram := make([]byte, 0x2000)
|
||||
if err := binary.Read(file, binary.LittleEndian, sram); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sram, nil
|
||||
}
|
||||
|
|
|
|||
104
util/util.go
104
util/util.go
|
|
@ -1,52 +1,52 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"image"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// https://stackoverflow.com/questions/9465815/rgb-to-yuv420-algorithm-efficiency
|
||||
|
||||
/*
|
||||
void rgba2yuv(void *destination, void *source, int width, int height, int stride) {
|
||||
const int image_size = width * height;
|
||||
unsigned char *rgba = source;
|
||||
unsigned char *dst_y = destination;
|
||||
unsigned char *dst_u = destination + image_size;
|
||||
unsigned char *dst_v = destination + image_size + image_size/4;
|
||||
|
||||
// Y plane
|
||||
for( int y=0; y<height; ++y ) {
|
||||
for( int x=0; x<width; ++x ) {
|
||||
const int i = y*(width+stride) + x;
|
||||
*dst_y++ = ( ( 66*rgba[4*i] + 129*rgba[4*i+1] + 25*rgba[4*i+2] ) >> 8 ) + 16;
|
||||
}
|
||||
}
|
||||
// U plane
|
||||
for( int y=0; y<height; y+=2 ) {
|
||||
for( int x=0; x<width; x+=2 ) {
|
||||
const int i = y*(width+stride) + x;
|
||||
*dst_u++ = ( ( -38*rgba[4*i] + -74*rgba[4*i+1] + 112*rgba[4*i+2] ) >> 8 ) + 128;
|
||||
}
|
||||
}
|
||||
// V plane
|
||||
for( int y=0; y<height; y+=2 ) {
|
||||
for( int x=0; x<width; x+=2 ) {
|
||||
const int i = y*(width+stride) + x;
|
||||
*dst_v++ = ( ( 112*rgba[4*i] + -94*rgba[4*i+1] + -18*rgba[4*i+2] ) >> 8 ) + 128;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// RgbaToYuv convert to yuv from rgba
|
||||
func RgbaToYuv(rgba *image.RGBA) []byte {
|
||||
w := rgba.Rect.Max.X
|
||||
h := rgba.Rect.Max.Y
|
||||
size := int(float32(w*h) * 1.5)
|
||||
stride := rgba.Stride - w*4
|
||||
yuv := make([]byte, size, size)
|
||||
C.rgba2yuv(unsafe.Pointer(&yuv[0]), unsafe.Pointer(&rgba.Pix[0]), C.int(w), C.int(h), C.int(stride))
|
||||
return yuv
|
||||
}
|
||||
package util
|
||||
|
||||
import (
|
||||
"image"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// https://stackoverflow.com/questions/9465815/rgb-to-yuv420-algorithm-efficiency
|
||||
|
||||
/*
|
||||
void rgba2yuv(void *destination, void *source, int width, int height, int stride) {
|
||||
const int image_size = width * height;
|
||||
unsigned char *rgba = source;
|
||||
unsigned char *dst_y = destination;
|
||||
unsigned char *dst_u = destination + image_size;
|
||||
unsigned char *dst_v = destination + image_size + image_size/4;
|
||||
|
||||
// Y plane
|
||||
for( int y=0; y<height; ++y ) {
|
||||
for( int x=0; x<width; ++x ) {
|
||||
const int i = y*(width+stride) + x;
|
||||
*dst_y++ = ( ( 66*rgba[4*i] + 129*rgba[4*i+1] + 25*rgba[4*i+2] ) >> 8 ) + 16;
|
||||
}
|
||||
}
|
||||
// U plane
|
||||
for( int y=0; y<height; y+=2 ) {
|
||||
for( int x=0; x<width; x+=2 ) {
|
||||
const int i = y*(width+stride) + x;
|
||||
*dst_u++ = ( ( -38*rgba[4*i] + -74*rgba[4*i+1] + 112*rgba[4*i+2] ) >> 8 ) + 128;
|
||||
}
|
||||
}
|
||||
// V plane
|
||||
for( int y=0; y<height; y+=2 ) {
|
||||
for( int x=0; x<width; x+=2 ) {
|
||||
const int i = y*(width+stride) + x;
|
||||
*dst_v++ = ( ( 112*rgba[4*i] + -94*rgba[4*i+1] + -18*rgba[4*i+2] ) >> 8 ) + 128;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
// RgbaToYuv convert to yuv from rgba
|
||||
func RgbaToYuv(rgba *image.RGBA) []byte {
|
||||
w := rgba.Rect.Max.X
|
||||
h := rgba.Rect.Max.Y
|
||||
size := int(float32(w*h) * 1.5)
|
||||
stride := rgba.Stride - w*4
|
||||
yuv := make([]byte, size, size)
|
||||
C.rgba2yuv(unsafe.Pointer(&yuv[0]), unsafe.Pointer(&rgba.Pix[0]), C.int(w), C.int(h), C.int(stride))
|
||||
return yuv
|
||||
}
|
||||
|
|
|
|||
520
webrtc/webrtc.go
520
webrtc/webrtc.go
|
|
@ -1,260 +1,260 @@
|
|||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
vpxEncoder "github.com/giongto35/game-online/vpx-encoder"
|
||||
"github.com/pions/webrtc"
|
||||
"github.com/pions/webrtc/pkg/media"
|
||||
)
|
||||
|
||||
var config = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}}
|
||||
|
||||
// Allows compressing offer/answer to bypass terminal input limits.
|
||||
const compress = false
|
||||
|
||||
func init() {
|
||||
//api.mediaEngine.RegisterDefaultCodecs()
|
||||
//webrtc.RegisterDefaultCodecs()
|
||||
}
|
||||
|
||||
func zip(in []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
gz := gzip.NewWriter(&b)
|
||||
_, err := gz.Write(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = gz.Flush()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = gz.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func unzip(in []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
_, err := b.Write(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r, err := gzip.NewReader(&b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
res, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Encode encodes the input in base64
|
||||
// It can optionally zip the input before encoding
|
||||
func Encode(obj interface{}) string {
|
||||
b, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if compress {
|
||||
b = zip(b)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// Decode decodes the input from base64
|
||||
// It can optionally unzip the input after decoding
|
||||
func Decode(in string, obj interface{}) {
|
||||
b, err := base64.StdEncoding.DecodeString(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if compress {
|
||||
b = unzip(b)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewWebRTC create
|
||||
func NewWebRTC() *WebRTC {
|
||||
w := &WebRTC{
|
||||
ImageChannel: make(chan []byte, 2),
|
||||
InputChannel: make(chan int, 2),
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// WebRTC connection
|
||||
type WebRTC struct {
|
||||
connection *webrtc.PeerConnection
|
||||
encoder *vpxEncoder.VpxEncoder
|
||||
isConnected bool
|
||||
isClosed bool
|
||||
// for yuvI420 image
|
||||
ImageChannel chan []byte
|
||||
InputChannel chan int
|
||||
}
|
||||
|
||||
// StartClient start webrtc
|
||||
func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println(err)
|
||||
w.StopClient()
|
||||
}
|
||||
}()
|
||||
|
||||
// reset client
|
||||
if w.isConnected {
|
||||
w.StopClient()
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
encoder, err := vpxEncoder.NewVpxEncoder(width, height, 20, 1200, 5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
w.encoder = encoder
|
||||
|
||||
fmt.Println("=== StartClient ===")
|
||||
|
||||
w.connection, err = webrtc.NewPeerConnection(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vp8Track, err := w.connection.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion2")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = w.connection.AddTrack(vp8Track)
|
||||
if err != nil {
|
||||
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 {
|
||||
go func() {
|
||||
w.isConnected = true
|
||||
fmt.Println("ConnectionStateConnected")
|
||||
w.startStreaming(vp8Track)
|
||||
}()
|
||||
|
||||
}
|
||||
if connectionState == webrtc.ICEConnectionStateFailed || connectionState == webrtc.ICEConnectionStateClosed || connectionState == webrtc.ICEConnectionStateDisconnected {
|
||||
w.StopClient()
|
||||
}
|
||||
})
|
||||
|
||||
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())
|
||||
|
||||
// Register channel opening handling
|
||||
d.OnOpen(func() {
|
||||
fmt.Printf("Data channel '%s'-'%d' open.\n", d.Label(), d.ID())
|
||||
})
|
||||
|
||||
// Register text message handling
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
//fmt.Printf("Message from DataChannel '%s': '%s' byte '%b'\n", d.Label(), string(msg.Data), msg.Data)
|
||||
i, _ := strconv.Atoi(string(msg.Data))
|
||||
w.InputChannel <- i
|
||||
})
|
||||
})
|
||||
|
||||
offer := webrtc.SessionDescription{}
|
||||
|
||||
Decode(remoteSession, &offer)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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===")
|
||||
w.isConnected = false
|
||||
if w.encoder != nil {
|
||||
w.encoder.Release()
|
||||
}
|
||||
if w.connection != nil {
|
||||
w.connection.Close()
|
||||
}
|
||||
w.connection = nil
|
||||
w.isClosed = true
|
||||
}
|
||||
|
||||
// IsConnected comment
|
||||
func (w *WebRTC) IsConnected() bool {
|
||||
return w.isConnected
|
||||
}
|
||||
|
||||
func (w *WebRTC) IsClosed() bool {
|
||||
return w.isClosed
|
||||
}
|
||||
|
||||
func (w *WebRTC) startStreaming(vp8Track *webrtc.Track) {
|
||||
fmt.Println("Start streaming")
|
||||
// send screenshot
|
||||
go func() {
|
||||
for w.isConnected {
|
||||
yuv := <-w.ImageChannel
|
||||
if len(w.encoder.Input) < cap(w.encoder.Input) {
|
||||
w.encoder.Input <- yuv
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// receive frame buffer
|
||||
go func() {
|
||||
for i := 0; w.isConnected; i++ {
|
||||
bs := <-w.encoder.Output
|
||||
vp8Track.WriteSample(media.Sample{Data: bs, Samples: 1})
|
||||
}
|
||||
}()
|
||||
}
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
vpxEncoder "github.com/giongto35/game-online/vpx-encoder"
|
||||
"github.com/pions/webrtc"
|
||||
"github.com/pions/webrtc/pkg/media"
|
||||
)
|
||||
|
||||
var config = webrtc.Configuration{ICEServers: []webrtc.ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}}}
|
||||
|
||||
// Allows compressing offer/answer to bypass terminal input limits.
|
||||
const compress = false
|
||||
|
||||
func init() {
|
||||
//api.mediaEngine.RegisterDefaultCodecs()
|
||||
//webrtc.RegisterDefaultCodecs()
|
||||
}
|
||||
|
||||
func zip(in []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
gz := gzip.NewWriter(&b)
|
||||
_, err := gz.Write(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = gz.Flush()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = gz.Close()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func unzip(in []byte) []byte {
|
||||
var b bytes.Buffer
|
||||
_, err := b.Write(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r, err := gzip.NewReader(&b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
res, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Encode encodes the input in base64
|
||||
// It can optionally zip the input before encoding
|
||||
func Encode(obj interface{}) string {
|
||||
b, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if compress {
|
||||
b = zip(b)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// Decode decodes the input from base64
|
||||
// It can optionally unzip the input after decoding
|
||||
func Decode(in string, obj interface{}) {
|
||||
b, err := base64.StdEncoding.DecodeString(in)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if compress {
|
||||
b = unzip(b)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, obj)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewWebRTC create
|
||||
func NewWebRTC() *WebRTC {
|
||||
w := &WebRTC{
|
||||
ImageChannel: make(chan []byte, 2),
|
||||
InputChannel: make(chan int, 2),
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// WebRTC connection
|
||||
type WebRTC struct {
|
||||
connection *webrtc.PeerConnection
|
||||
encoder *vpxEncoder.VpxEncoder
|
||||
isConnected bool
|
||||
isClosed bool
|
||||
// for yuvI420 image
|
||||
ImageChannel chan []byte
|
||||
InputChannel chan int
|
||||
}
|
||||
|
||||
// StartClient start webrtc
|
||||
func (w *WebRTC) StartClient(remoteSession string, width, height int) (string, error) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println(err)
|
||||
w.StopClient()
|
||||
}
|
||||
}()
|
||||
|
||||
// reset client
|
||||
if w.isConnected {
|
||||
w.StopClient()
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
encoder, err := vpxEncoder.NewVpxEncoder(width, height, 20, 1200, 5)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
w.encoder = encoder
|
||||
|
||||
fmt.Println("=== StartClient ===")
|
||||
|
||||
w.connection, err = webrtc.NewPeerConnection(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
vp8Track, err := w.connection.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "video", "pion2")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = w.connection.AddTrack(vp8Track)
|
||||
if err != nil {
|
||||
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 {
|
||||
go func() {
|
||||
w.isConnected = true
|
||||
fmt.Println("ConnectionStateConnected")
|
||||
w.startStreaming(vp8Track)
|
||||
}()
|
||||
|
||||
}
|
||||
if connectionState == webrtc.ICEConnectionStateFailed || connectionState == webrtc.ICEConnectionStateClosed || connectionState == webrtc.ICEConnectionStateDisconnected {
|
||||
w.StopClient()
|
||||
}
|
||||
})
|
||||
|
||||
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())
|
||||
|
||||
// Register channel opening handling
|
||||
d.OnOpen(func() {
|
||||
fmt.Printf("Data channel '%s'-'%d' open.\n", d.Label(), d.ID())
|
||||
})
|
||||
|
||||
// Register text message handling
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
//fmt.Printf("Message from DataChannel '%s': '%s' byte '%b'\n", d.Label(), string(msg.Data), msg.Data)
|
||||
i, _ := strconv.Atoi(string(msg.Data))
|
||||
w.InputChannel <- i
|
||||
})
|
||||
})
|
||||
|
||||
offer := webrtc.SessionDescription{}
|
||||
|
||||
Decode(remoteSession, &offer)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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===")
|
||||
w.isConnected = false
|
||||
if w.encoder != nil {
|
||||
w.encoder.Release()
|
||||
}
|
||||
if w.connection != nil {
|
||||
w.connection.Close()
|
||||
}
|
||||
w.connection = nil
|
||||
w.isClosed = true
|
||||
}
|
||||
|
||||
// IsConnected comment
|
||||
func (w *WebRTC) IsConnected() bool {
|
||||
return w.isConnected
|
||||
}
|
||||
|
||||
func (w *WebRTC) IsClosed() bool {
|
||||
return w.isClosed
|
||||
}
|
||||
|
||||
func (w *WebRTC) startStreaming(vp8Track *webrtc.Track) {
|
||||
fmt.Println("Start streaming")
|
||||
// send screenshot
|
||||
go func() {
|
||||
for w.isConnected {
|
||||
yuv := <-w.ImageChannel
|
||||
if len(w.encoder.Input) < cap(w.encoder.Input) {
|
||||
w.encoder.Input <- yuv
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// receive frame buffer
|
||||
go func() {
|
||||
for i := 0; w.isConnected; i++ {
|
||||
bs := <-w.encoder.Output
|
||||
vp8Track.WriteSample(media.Sample{Data: bs, Samples: 1})
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue