remove candidate, add limit frame, add multi game

This commit is contained in:
trichimtrich 2019-04-06 05:21:16 +08:00
parent 81444a405b
commit 1048846266
22 changed files with 1214 additions and 1164 deletions

View file

@ -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

Binary file not shown.

BIN
games/Contra.nes Normal file

Binary file not shown.

BIN
games/Kirby's Adventure.nes Normal file

Binary file not shown.

BIN
games/Mega Man 2.nes Normal file

Binary file not shown.

BIN
games/Mega Man.nes Normal file

Binary file not shown.

BIN
games/Metal Gear.nes Normal file

Binary file not shown.

BIN
games/Mike Tyson.nes Normal file

Binary file not shown.

BIN
games/Mortal Kombat 4.nes Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
games/Super Mario Bros.nes Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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
View file

@ -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
}
}
}

View file

@ -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))
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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
}

View file

@ -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
}

View file

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