diff --git a/Dockerfile b/Dockerfile index 3d196e71..200d60b8 100644 --- a/Dockerfile +++ b/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 diff --git a/games/1200-in-1.nes b/games/1200-in-1.nes new file mode 100644 index 00000000..d660c97c Binary files /dev/null and b/games/1200-in-1.nes differ diff --git a/games/Contra.nes b/games/Contra.nes new file mode 100644 index 00000000..5a154db6 Binary files /dev/null and b/games/Contra.nes differ diff --git a/games/Kirby's Adventure.nes b/games/Kirby's Adventure.nes new file mode 100644 index 00000000..f2ac915a Binary files /dev/null and b/games/Kirby's Adventure.nes differ diff --git a/games/Mega Man 2.nes b/games/Mega Man 2.nes new file mode 100644 index 00000000..ea261a3f Binary files /dev/null and b/games/Mega Man 2.nes differ diff --git a/games/Mega Man.nes b/games/Mega Man.nes new file mode 100644 index 00000000..0669d5f4 Binary files /dev/null and b/games/Mega Man.nes differ diff --git a/games/Metal Gear.nes b/games/Metal Gear.nes new file mode 100644 index 00000000..0b10e689 Binary files /dev/null and b/games/Metal Gear.nes differ diff --git a/games/Mike Tyson.nes b/games/Mike Tyson.nes new file mode 100644 index 00000000..658ba89a Binary files /dev/null and b/games/Mike Tyson.nes differ diff --git a/games/Mortal Kombat 4.nes b/games/Mortal Kombat 4.nes new file mode 100644 index 00000000..ee03f073 Binary files /dev/null and b/games/Mortal Kombat 4.nes differ diff --git a/games/Super Mario Bros 2.nes b/games/Super Mario Bros 2.nes new file mode 100644 index 00000000..58a7a3d2 Binary files /dev/null and b/games/Super Mario Bros 2.nes differ diff --git a/games/Super Mario Bros 3.nes b/games/Super Mario Bros 3.nes new file mode 100644 index 00000000..d0f6a272 Binary files /dev/null and b/games/Super Mario Bros 3.nes differ diff --git a/games/Super Mario Bros.nes b/games/Super Mario Bros.nes new file mode 100644 index 00000000..878ef21b Binary files /dev/null and b/games/Super Mario Bros.nes differ diff --git a/games/Teenage Mutant Ninja Turtles 3.nes b/games/Teenage Mutant Ninja Turtles 3.nes new file mode 100644 index 00000000..c57e9454 Binary files /dev/null and b/games/Teenage Mutant Ninja Turtles 3.nes differ diff --git a/games/VS Super Mario Bros.nes b/games/VS Super Mario Bros.nes new file mode 100644 index 00000000..55a0dbf5 Binary files /dev/null and b/games/VS Super Mario Bros.nes differ diff --git a/index.html b/index.html index 9bc1a4bd..fa7d4806 100644 --- a/index.html +++ b/index.html @@ -1,266 +1,299 @@ - - - - -

-
- Use Up, Down, Left, Right to Move
- Z to jump (A)
- X to sprint (B)
- C is start button
- V is select button
- - Fullscreen media for better gaming experience
-
- - -
- -
- Refresh to retry -
- - + + + + + + + + +
+ +

+
+ Use Up, Down, Left, Right to Move
+ Z to jump (A)
+ X to sprint (B)
+ C is start button
+ V is select button
+ + Fullscreen media for better gaming experience
+
+ + +
+ +
+ Refresh to retry +
+ + diff --git a/main.go b/main.go index e2e29a66..c91f2b77 100644 --- a/main.go +++ b/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 + } + } +} diff --git a/ui/director.go b/ui/director.go index 6afcd6f3..30be463b 100644 --- a/ui/director.go +++ b/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)) +} diff --git a/ui/gameview.go b/ui/gameview.go index 313b3173..41540296 100644 --- a/ui/gameview.go +++ b/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) +} diff --git a/ui/run.go b/ui/run.go index 87bea59b..65ea1b9f 100644 --- a/ui/run.go +++ b/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) +} diff --git a/ui/util.go b/ui/util.go index dc0e20eb..f9fca8b8 100644 --- a/ui/util.go +++ b/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 +} diff --git a/util/util.go b/util/util.go index a218574d..43c5f9dc 100644 --- a/util/util.go +++ b/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> 8 ) + 16; - } - } - // U plane - for( int y=0; y> 8 ) + 128; - } - } - // V plane - for( int y=0; y> 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> 8 ) + 16; + } + } + // U plane + for( int y=0; y> 8 ) + 128; + } + } + // V plane + for( int y=0; y> 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 +} diff --git a/webrtc/webrtc.go b/webrtc/webrtc.go index 3c53aa0d..1a717ffe 100644 --- a/webrtc/webrtc.go +++ b/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}) + } + }() +}