From 1ea671aeb1348ed8f540776a9848b882d440b870 Mon Sep 17 00:00:00 2001 From: Srayan Jana Date: Sun, 11 May 2025 07:12:03 +0300 Subject: [PATCH 1/7] Add Ebiten game example --- ebiten-game/LICENSE.md | 7 + ebiten-game/README.md | 19 + ebiten-game/game/.gitignore | 27 ++ ebiten-game/game/gopher.png | Bin 0 -> 43718 bytes ebiten-game/game/index.html | 21 + ebiten-game/game/main.go | 532 ++++++++++++++++++++++++ ebiten-game/game/ui.go | 64 +++ ebiten-game/signaling-server/.gitignore | 25 ++ ebiten-game/signaling-server/README.md | 3 + ebiten-game/signaling-server/main.go | 330 +++++++++++++++ go.mod | 34 +- go.sum | 111 +++-- 12 files changed, 1127 insertions(+), 46 deletions(-) create mode 100644 ebiten-game/LICENSE.md create mode 100644 ebiten-game/README.md create mode 100644 ebiten-game/game/.gitignore create mode 100644 ebiten-game/game/gopher.png create mode 100644 ebiten-game/game/index.html create mode 100644 ebiten-game/game/main.go create mode 100644 ebiten-game/game/ui.go create mode 100644 ebiten-game/signaling-server/.gitignore create mode 100644 ebiten-game/signaling-server/README.md create mode 100644 ebiten-game/signaling-server/main.go diff --git a/ebiten-game/LICENSE.md b/ebiten-game/LICENSE.md new file mode 100644 index 00000000..ad4c1e7c --- /dev/null +++ b/ebiten-game/LICENSE.md @@ -0,0 +1,7 @@ +## game/gopher.png + +``` +The Go gopher was designed by Renee French. (http://reneefrench.blogspot.com/) +The design is licensed under the Creative Commons 4.0 Attributions license. +Read this article for more details: https://blog.golang.org/gopher +``` \ No newline at end of file diff --git a/ebiten-game/README.md b/ebiten-game/README.md new file mode 100644 index 00000000..bb56a2b0 --- /dev/null +++ b/ebiten-game/README.md @@ -0,0 +1,19 @@ +# Ebitengine Game! + +This is a pretty nifty demo on how to use [ebitengine](https://ebitengine.org/) and [pion](https://github.com/pion/webrtc) to pull off a cross platform game! + +You can have a client running on the browser and one running on a desktop and they can talk to each other, provided they are connected to the same signaling server + +Requires the signaling server to be running. To do go, just go inside the folder /signaling-server and do ``go run .`` + +you can then run the game by going in /game and doing either + +``go run .`` for running the game on desktop + +(see [this tutorial for more information on how to build for WebAssembly](https://ebitengine.org/en/documents/webassembly.html)) + +Click "Host Game" to get the lobby id, and then share that with the other clients to get connected + +To play: Just move around with the arrow keys once you have connected! + +Right now this only supports two clients in the same lobby diff --git a/ebiten-game/game/.gitignore b/ebiten-game/game/.gitignore new file mode 100644 index 00000000..cf14364f --- /dev/null +++ b/ebiten-game/game/.gitignore @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2025 The Pion community +# SPDX-License-Identifier: MIT + +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.wasm + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum +wasm_exec.js diff --git a/ebiten-game/game/gopher.png b/ebiten-game/game/gopher.png new file mode 100644 index 0000000000000000000000000000000000000000..b11c17039442e0972a4756043cdd3ebb38cefb67 GIT binary patch literal 43718 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 HF!=uf$Few2 literal 0 HcmV?d00001 diff --git a/ebiten-game/game/index.html b/ebiten-game/game/index.html new file mode 100644 index 00000000..e6b444c4 --- /dev/null +++ b/ebiten-game/game/index.html @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/ebiten-game/game/main.go b/ebiten-game/game/main.go new file mode 100644 index 00000000..b2143745 --- /dev/null +++ b/ebiten-game/game/main.go @@ -0,0 +1,532 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "strconv" + + //"runtime" + + //"github.com/pion/randutil" + + _ "image/jpeg" + _ "image/png" + "io" + "log" + "os" + "time" + + "github.com/ebitengine/debugui" + "github.com/pion/webrtc/v4" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/kelindar/binary" + //"github.com/hajimehoshi/ebiten/v2/inpututil" +) + +var img *ebiten.Image + +var ( + pos_x = 40.0 + pos_y = 40.0 + remote_pos_x = 40.0 + remote_pos_y = 40.0 +) + +var lobby_id string + +var signalingIP = "127.0.0.1" +var port = 3000 + +func getSignalingURL() string { + return "http://" + signalingIP + ":" + strconv.Itoa(port) +} + +// players registered by host +var registered_players = make(map[int]struct{}) + +// client to the HTTP signaling server +var httpClient = &http.Client{ + Timeout: 10 * time.Second, +} + +func init() { + var err error + img, _, err = ebitenutil.NewImageFromFile("gopher.png") + if err != nil { + log.Fatal(err) + } +} + +// implements ebiten.Game interface +type Game struct { + debugUI debugui.DebugUI + inputCapturingState debugui.InputCapturingState + + logBuf string + logSubmitBuf string + logUpdated bool + + lobby_id string + isHost bool + + localDebugInformation string + remoteDebugInformation string +} + +func NewGame() (*Game, error) { + g := &Game{} + + return g, nil +} + +// Layout implements Game. +func (g *Game) Layout(outsideWidth int, outsideHeight int) (int, int) { + return outsideWidth, outsideHeight +} + +// called every tick (default 60 times a second) +// updates game logical state +func (g *Game) Update() error { + + if ebiten.IsKeyPressed(ebiten.KeyUp) { + pos_y -= 1 + } + + if ebiten.IsKeyPressed(ebiten.KeyDown) { + pos_y += 1 + } + + if ebiten.IsKeyPressed(ebiten.KeyLeft) { + pos_x -= 1 + } + + if ebiten.IsKeyPressed(ebiten.KeyRight) { + pos_x += 1 + } + + inputCaptured, err := g.debugUI.Update(func(ctx *debugui.Context) error { + g.logWindow(ctx) + return nil + }) + if err != nil { + return err + } + g.inputCapturingState = inputCaptured + return nil +} + +// called every frame, depends on the monitor refresh rate +// which will probably be at least 60 times per second +func (g *Game) Draw(screen *ebiten.Image) { + // prints something on the screen + debugString := fmt.Sprintf("FPS: %f", ebiten.ActualFPS()) + debugString += "\n" + g.localDebugInformation + "\n" + g.remoteDebugInformation + ebitenutil.DebugPrint(screen, debugString) + + // draw image + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(pos_x, pos_y) + screen.DrawImage(img, op) + + // draw remote + op2 := &ebiten.DrawImageOptions{} + op2.GeoM.Translate(remote_pos_x, remote_pos_y) + screen.DrawImage(img, op2) + + g.debugUI.Draw(screen) +} + +var ( + // probably move all webrtc networking stuff to a struct i can manage + peerConnection *webrtc.PeerConnection +) + +const messageSize = 32 + +type PlayerData struct { + Id int +} + +func (game *Game) startConnection() { + // Since this behavior diverges from the WebRTC API it has to be + // enabled using a settings engine. Mixing both detached and the + // OnMessage DataChannel API is not supported. + + // Create a SettingEngine and enable Detach + s := webrtc.SettingEngine{} + s.DetachDataChannels() + + // Create an API object with the engine + api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) + + // Everything below is the Pion WebRTC API! Thanks for using it ❤️. + + // Prepare the configuration + config := webrtc.Configuration{ + ICEServers: []webrtc.ICEServer{ + { + URLs: []string{"stun:stun.l.google.com:19302"}, + }, + }, + } + + // Create a new RTCPeerConnection using the API object + pc, err := api.NewPeerConnection(config) + if err != nil { + panic(err) + } + + // Set the global variable to the newly created RTCPeerConnection + peerConnection = pc + + // Set the handler for Peer connection state + // This will notify you when the peer has connected/disconnected + peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { + game.writeLog(fmt.Sprintf("Peer Connection State has changed: %s\n", s.String())) + + if s == webrtc.PeerConnectionStateFailed { + // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. + // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. + // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. + game.writeLog(fmt.Sprintln("Peer Connection has gone to failed exiting")) + os.Exit(0) + } + + if s == webrtc.PeerConnectionStateClosed { + // PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify + game.writeLog(fmt.Sprintln("Peer Connection has gone to closed exiting")) + os.Exit(0) + } + }) + + // Set the handler for ICE connection state + // This will notify you when the peer has connected/disconnected + peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { + game.writeLog(fmt.Sprintf("ICE Connection State has changed: %s\n", connectionState.String())) + }) + + // the one that gives the answer is the host + if game.isHost { + game.writeLog("Hosting a lobby") + // Host creates lobby + lobby_resp, err := httpClient.Get(getSignalingURL() + "/lobby/host") + if err != nil { + panic(err) + } + bodyBytes, err := io.ReadAll(lobby_resp.Body) + if err != nil { + panic(err) + } + lobby_id = string(bodyBytes) + lobby_id_str := fmt.Sprintf("Lobby ID: %s\n", lobby_id) + game.writeLog(lobby_id_str) + + // Register data channel creation handling + peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { + game.writeLog(fmt.Sprintf("New DataChannel %s %d\n", d.Label(), d.ID())) + + // Register channel opening handling + d.OnOpen(func() { + + s := fmt.Sprintf("Data channel '%s'-'%d' open on host side!", d.Label(), d.ID()) + game.writeLog(s) + + // Detach the data channel + raw, dErr := d.Detach() + if dErr != nil { + panic(dErr) + } + + // Handle reading from the data channel + go ReadLoop(game, raw) + + // Handle writing to the data channel + go WriteLoop(game, raw) + }) + }) + + // poll for offer from signaling server for player + pollForPlayerOffer := func(player_id int) { + ticker := time.NewTicker(1 * time.Second) + for { + select { + case t := <-ticker.C: + game.writeLog(fmt.Sprintln("Tick at", t)) + game.writeLog(fmt.Sprintf("Polling for offer for %d\n", player_id)) + // hardcode that there is only one other player and they have player_id 1 + getUrl := getSignalingURL() + "/offer/get?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_id) + game.writeLog(fmt.Sprintln(getUrl)) + offer_resp, err := httpClient.Get(getUrl) + if err != nil { + panic(err) + } + if offer_resp.StatusCode != http.StatusOK { + continue + } + body := new(bytes.Buffer) + body.ReadFrom(offer_resp.Body) + game.writeLog(fmt.Sprintf("Got offer %v\n", body.String())) + offer := webrtc.SessionDescription{} + err = json.NewDecoder(body).Decode(&offer) + if err != nil { + panic(err) + } + // Set the remote SessionDescription + err = peerConnection.SetRemoteDescription(offer) + if err != nil { + panic(err) + } + // Create answer + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } + + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + + // Sets the LocalDescription, and starts our UDP listeners + err = peerConnection.SetLocalDescription(answer) + if err != nil { + panic(err) + } + + // Block until ICE Gathering is complete, disabling trickle ICE + // we do this because we only can exchange one signaling message + // in a production application you should exchange ICE Candidates via OnICECandidate + <-gatherComplete + // send answer we generated to the signaling server + answerJson, err := json.Marshal(peerConnection.LocalDescription()) + if err != nil { + panic(err) + } + postUrl := getSignalingURL() + "/answer/post?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_id) + game.writeLog(fmt.Sprintln(postUrl)) + httpClient.Post(postUrl, "application/json", bytes.NewBuffer(answerJson)) + // if we have successfully set the remote description, we can break out of the loop + ticker.Stop() + return + } + } + } + + go func() { + ticker := time.NewTicker(1 * time.Second) + for { + select { + case t := <-ticker.C: + game.writeLog(fmt.Sprintln("Polling for lobby ID {", lobby_id, "} at", t)) + idUrl := getSignalingURL() + "/lobby/unregisteredPlayers?id=" + lobby_id + game.writeLog(fmt.Sprintln(idUrl)) + id_resp, err := httpClient.Get(idUrl) + if err != nil { + panic(err) + } + if id_resp.StatusCode != http.StatusOK { + continue + } + var player_ids []int + err = json.NewDecoder(id_resp.Body).Decode(&player_ids) + if err != nil { + panic(err) + } + game.writeLog(fmt.Sprintf("Player IDs: %v\n", player_ids)) + // poll for all of the unregistered players + for _, player_id := range player_ids { + // only start goroutine if player_id hasn't been registered yet + if _, ok := registered_players[player_id]; !ok { + registered_players[player_id] = struct{}{} + go pollForPlayerOffer(player_id) + } + } + } + } + }() + } else { + game.writeLog("Joining lobby: " + lobby_id) + // the following is for the client joining the lobby + // get lobby id from text input + lobby_id = game.lobby_id + response, err := httpClient.Get(getSignalingURL() + "/lobby/join?id=" + lobby_id) + if err != nil { + panic(err) + } + var player_data PlayerData + err = json.NewDecoder(response.Body).Decode(&player_data) + if err != nil { + panic(err) + } + game.writeLog(fmt.Sprintf("Player ID: %v\n", player_data)) + // Create a datachannel with label 'data' + dataChannel, err := peerConnection.CreateDataChannel("data", nil) + if err != nil { + panic(err) + } + + // Register channel opening handling + dataChannel.OnOpen(func() { + s := fmt.Sprintf("Data channel '%s'-'%d' open on client side!", dataChannel.Label(), dataChannel.ID()) + game.writeLog(s) + + // Detach the data channel + raw, dErr := dataChannel.Detach() + if dErr != nil { + panic(dErr) + } + + // Handle reading from the data channel + go ReadLoop(game, raw) + + // Handle writing to the data channel + go WriteLoop(game, raw) + }) + + // Create an offer to send to the browser + offer, err := peerConnection.CreateOffer(nil) + if err != nil { + panic(err) + } + + // Sets the LocalDescription, and starts our UDP listeners + err = peerConnection.SetLocalDescription(offer) + if err != nil { + panic(err) + } + + // print out possible offers from different ICE Candidates + peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { + if candidate != nil { + offerJson, err := json.Marshal(peerConnection.LocalDescription()) + if err != nil { + panic(err) + } + postUrl := getSignalingURL() + "/offer/post?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_data.Id) + game.writeLog(fmt.Sprintln(postUrl)) + httpClient.Post(postUrl, "application/json", bytes.NewBuffer(offerJson)) + } + }) + + answer := webrtc.SessionDescription{} + // read answer from other peer (wait till we actually get something) + ticker := time.NewTicker(1 * time.Second) + go func() { + for { + select { + case t := <-ticker.C: + game.writeLog(fmt.Sprintln("Tick at", t)) + game.writeLog(fmt.Sprintln("Polling for answer")) + url := getSignalingURL() + "/answer/get?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_data.Id) + fmt.Println(url) + answer_resp, err := httpClient.Get(url) + if err != nil { + panic(err) + } + if answer_resp.StatusCode != http.StatusOK { + continue + } + body := new(bytes.Buffer) + body.ReadFrom(answer_resp.Body) + game.writeLog(fmt.Sprintf("Got answer %v\n", body.String())) + err = json.NewDecoder(body).Decode(&answer) + if err != nil { + panic(err) + } + + if err := peerConnection.SetRemoteDescription(answer); err != nil { + panic(err) + } + + // if we have successfully set the remote description, we can break out of the loop + ticker.Stop() + return + } + } + }() + } +} + +func (g *Game) closeConnection() { + if cErr := peerConnection.Close(); cErr != nil { + fmt.Printf("cannot close peerConnection: %v\n", cErr) + } + // TODO: this doesn't work, fix this + if g.isHost { + // delete lobby if host + url := getSignalingURL() + "/lobby/delete" + fmt.Println(url) + httpClient.Get(url) + } +} + +// entry point of the program +func main() { + ebiten.SetWindowSize(640, 480) + ebiten.SetWindowTitle("Hello, World!") + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + + g, err := NewGame() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := ebiten.RunGame(g); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + // close the connection when the game ends + g.closeConnection() +} + +type Packet struct { + Pos_x float64 + Pos_y float64 +} + +// ReadLoop shows how to read from the datachannel directly +func ReadLoop(g *Game, d io.Reader) { + for { + buffer := make([]byte, messageSize) + _, err := io.ReadFull(d, buffer) + if err != nil { + g.writeLog(fmt.Sprintln("Datachannel closed; Exit the readloop:", err)) + return + } + + var packet Packet + err = binary.Unmarshal(buffer, &packet) + if err != nil { + panic(err) + } + + remote_pos_x = packet.Pos_x + remote_pos_y = packet.Pos_y + + g.remoteDebugInformation = fmt.Sprintf("Message from DataChannel: %f %f", packet.Pos_x, packet.Pos_y) + } +} + +// WriteLoop shows how to write to the datachannel directly +func WriteLoop(g *Game, d io.Writer) { + ticker := time.NewTicker(time.Millisecond * 20) + defer ticker.Stop() + for range ticker.C { + packet := &Packet{pos_x, pos_y} + g.localDebugInformation = fmt.Sprintf("Sending x:%f y:%f", packet.Pos_x, packet.Pos_y) + encoded, err := binary.Marshal(packet) + if err != nil { + panic(err) + } + + if _, err := d.Write(encoded); err != nil { + panic(err) + } + } +} diff --git a/ebiten-game/game/ui.go b/ebiten-game/game/ui.go new file mode 100644 index 00000000..2dfd9486 --- /dev/null +++ b/ebiten-game/game/ui.go @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2024 The Ebitengine Authors +// SPDX-License-Identifier: MIT + +package main + +import ( + "image" + + "github.com/ebitengine/debugui" + "github.com/hajimehoshi/ebiten/v2" +) + +func (g *Game) writeLog(text string) { + if len(g.logBuf) > 0 { + g.logBuf += "\n" + } + g.logBuf += text + g.logUpdated = true +} + +func (g *Game) logWindow(ctx *debugui.Context) { + ctx.Window("Log Window", image.Rect(350, 40, 650, 290), func(layout debugui.ContainerLayout) { + ctx.SetGridLayout([]int{-1}, []int{-1, 0}) + ctx.Panel(func(layout debugui.ContainerLayout) { + ctx.SetGridLayout([]int{-1}, []int{-1}) + ctx.Text(g.logBuf) + if g.logUpdated { + ctx.SetScroll(image.Pt(layout.ScrollOffset.X, layout.ContentSize.Y)) + g.logUpdated = false + } + }) + ctx.GridCell(func(bounds image.Rectangle) { + submit_open := func() { + g.isHost = true + g.startConnection() + } + + submit_join := func() { + g.isHost = false + if g.logSubmitBuf == "" { + return + } + g.lobby_id = g.logSubmitBuf + g.logSubmitBuf = "" + g.startConnection() + } + + ctx.SetGridLayout([]int{-1, -1, -1, -1}, nil) + ctx.Text("Lobby ID:") + ctx.TextField(&g.logSubmitBuf).On(func() { + if ebiten.IsKeyPressed(ebiten.KeyEnter) { + submit_join() + ctx.SetTextFieldValue(g.logSubmitBuf) + } + }) + ctx.Button("Open").On(func() { + submit_open() + }) + ctx.Button("Join").On(func() { + submit_join() + }) + }) + }) +} diff --git a/ebiten-game/signaling-server/.gitignore b/ebiten-game/signaling-server/.gitignore new file mode 100644 index 00000000..d70c6d48 --- /dev/null +++ b/ebiten-game/signaling-server/.gitignore @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: 2025 The Pion community +# SPDX-License-Identifier: MIT + +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work +go.work.sum diff --git a/ebiten-game/signaling-server/README.md b/ebiten-game/signaling-server/README.md new file mode 100644 index 00000000..cc8138ac --- /dev/null +++ b/ebiten-game/signaling-server/README.md @@ -0,0 +1,3 @@ +# go signaling server + +to run just do ``go run .`` diff --git a/ebiten-game/signaling-server/main.go b/ebiten-game/signaling-server/main.go new file mode 100644 index 00000000..8dc45dce --- /dev/null +++ b/ebiten-game/signaling-server/main.go @@ -0,0 +1,330 @@ +// SPDX-FileCopyrightText: 2025 The Pion community +// SPDX-License-Identifier: MIT + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "math/rand" + "net/http" + "strconv" + "sync" + + "github.com/pion/webrtc/v4" + "github.com/rs/cors" +) + +type ClientConnection struct { + IsHost bool + Offer *webrtc.SessionDescription + Answer *webrtc.SessionDescription +} + +type Lobby struct { + mutex sync.Mutex + // host is first client in lobby.Clients + Clients []ClientConnection +} + +var lobby_list = map[string]*Lobby{} + +type PlayerData struct { + // player id is index in lobby.Clients + Id int +} + +var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +func generateNewLobbyId() string { + // have random size for lobby id + size := 6 + buffer := make([]rune, size) + for i := range buffer { + buffer[i] = letters[rand.Intn(len(letters))] + } + id := string(buffer) + + // check if room id is already in lobby_list + _, ok := lobby_list[id] + if ok { + // if it already exists, call function again + return generateNewLobbyId() + } + return id +} + +func makeLobby() string { + lobby := Lobby{} + lobby.Clients = []ClientConnection{} + // first client is always host + lobby_id := generateNewLobbyId() + lobby_list[lobby_id] = &lobby + return lobby_id +} + +func getLobbyIds() []string { + lobbies := make([]string, len(lobby_list)) + i := 0 + for k := range lobby_list { + lobbies[i] = k + i++ + } + return lobbies +} + +func main() { + mux := http.NewServeMux() + mux.Handle("/", http.FileServer(http.Dir("./public"))) + mux.HandleFunc("/lobby/host", lobbyHost) + mux.HandleFunc("/lobby/join", lobbyJoin) + mux.HandleFunc("/lobby/delete", lobbyDelete) + mux.HandleFunc("/lobby/unregisteredPlayers", lobbyUnregisteredPlayers) + mux.HandleFunc("/offer/get", offerGet) + mux.HandleFunc("/offer/post", offerPost) + mux.HandleFunc("/answer/get", answerGet) + mux.HandleFunc("/answer/post", answerPost) + mux.HandleFunc("/ice", ice) + + fmt.Println("Server started on port 3000") + // cors.Default() setup the middleware with default options being + // all origins accepted with simple methods (GET, POST). See + // documentation below for more options. + handler := cors.Default().Handler(mux) + http.ListenAndServe(":3000", handler) +} + +func lobbyHost(w http.ResponseWriter, r *http.Request) { + lobby_id := makeLobby() + lobby := lobby_list[lobby_id] + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + // host is first client in lobby.Clients + lobby.Clients = append(lobby.Clients, ClientConnection{IsHost: true}) + // return lobby id to host + io.Writer.Write(w, []byte(lobby_id)) + fmt.Println("lobbyHost") + fmt.Printf("lobby added: %s\n", lobby_id) + // print all lobbies + fmt.Printf("lobby_list:%s\n", getLobbyIds()) +} + +// call "/lobby?id={lobby_id}" to connect to lobby +func lobbyJoin(w http.ResponseWriter, r *http.Request) { + fmt.Println("lobbyJoin") + w.Header().Set("Content-Type", "application/json") + // https://freshman.tech/snippets/go/extract-url-query-params/ + // get lobby id from query params + lobby_id := r.URL.Query().Get("id") + fmt.Printf("lobby_id: %s\n", lobby_id) + + // only continue with connection if lobby exists + lobby, ok := lobby_list[lobby_id] + // If the key doesn't exist, return error + if !ok { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Lobby not found")) + return + } + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + body, err := io.ReadAll(r.Body) + if err != nil { + fmt.Printf("Failed to read body: %s", err) + return + } + + fmt.Printf("body: %s", body) + + // send player id once generated + lobby.Clients = append(lobby.Clients, ClientConnection{IsHost: false}) + // player id is index in lobby.Clients + player_id := len(lobby.Clients) - 1 + fmt.Printf("player_id: %d\n", player_id) + fmt.Println(lobby.Clients) + player_data := PlayerData{Id: player_id} + jsonValue, _ := json.Marshal(player_data) + io.Writer.Write(w, jsonValue) +} + +func lobbyDelete(w http.ResponseWriter, r *http.Request) { + fmt.Println("lobbyDelete") + w.Header().Set("Content-Type", "application/json") + // https://freshman.tech/snippets/go/extract-url-query-params/ + // get lobby id from query params + lobby_id := r.URL.Query().Get("id") + fmt.Printf("lobby_id: %s\n", lobby_id) + // delete lobby + delete(lobby_list, lobby_id) + fmt.Printf("lobby_list:%s\n", getLobbyIds()) +} + +// return players who haven't been registered yet by the host +func lobbyUnregisteredPlayers(w http.ResponseWriter, r *http.Request) { + fmt.Println("UnregisteredPlayers") + w.Header().Set("Content-Type", "application/json") + // https://freshman.tech/snippets/go/extract-url-query-params/ + // get lobby id from query params + lobby_id := r.URL.Query().Get("id") + lobby := lobby_list[lobby_id] + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + // get all players who haven't been registered yet + player_ids := []int{} + for i, client := range lobby.Clients { + if !client.IsHost && client.Answer == nil { + player_ids = append(player_ids, i) + } + } + + // return lobby id to host + jsonValue, _ := json.Marshal(player_ids) + io.Writer.Write(w, jsonValue) + fmt.Printf("player_ids %v\n", player_ids) +} + +func validatePlayer(w http.ResponseWriter, r *http.Request) (*Lobby, int, error) { + fmt.Println("validatePlayer") + lobby_id := r.URL.Query().Get("lobby_id") + //fmt.Printf("lobby_id: %s\n", lobby_id) + + // only continue with connection if lobby exists + lobby, ok := lobby_list[lobby_id] + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + // If the key doesn't exist, return error + if !ok { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Lobby not found")) + return nil, 0, errors.New("Lobby not found") + } + + player_id_string := r.URL.Query().Get("player_id") + player_id, err := strconv.Atoi(player_id_string) + if err != nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Player not found")) + return nil, 0, errors.New("Player not found") + } + //fmt.Printf("player_id: %d\n", player_id) + //fmt.Printf("length of lobby.Clients: %d\n", len(lobby_list[lobby_id].Clients)) + //fmt.Println(lobby.Clients) + // check if player actually exists + if player_id < 0 || player_id >= len(lobby.Clients) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Player not found")) + return nil, 0, errors.New("Player not found") + } + return lobby, player_id, nil +} + +func offerGet(w http.ResponseWriter, r *http.Request) { + fmt.Println("offerGet") + w.Header().Set("Content-Type", "application/json") + + lobby, player_id, err := validatePlayer(w, r) + if err != nil { + return + } + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + offer := lobby.Clients[player_id].Offer + if offer == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Offer not found")) + return + } + + jsonValue, _ := json.Marshal(offer) + + io.Writer.Write(w, jsonValue) + + /* + fmt.Println("offerGet") + fmt.Println(jsonValue) + */ +} + +func offerPost(w http.ResponseWriter, r *http.Request) { + fmt.Println("offerPost") + + lobby, player_id, err := validatePlayer(w, r) + if err != nil { + return + } + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + var sdp webrtc.SessionDescription + + // Try to decode the request body into the struct. If there is an error, + // respond to the client with the error message and a 400 status code. + err = json.NewDecoder(r.Body).Decode(&sdp) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + lobby.Clients[player_id].Offer = &sdp + fmt.Printf("Lobby: %+v\n", lobby.Clients) +} + +func answerGet(w http.ResponseWriter, r *http.Request) { + fmt.Println("answerGet") + w.Header().Set("Content-Type", "application/json") + + lobby, player_id, err := validatePlayer(w, r) + if err != nil { + return + } + + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + answer := lobby.Clients[player_id].Answer + if answer == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 - Answer not found")) + return + } + + jsonValue, _ := json.Marshal(answer) + + io.Writer.Write(w, jsonValue) +} + +func answerPost(w http.ResponseWriter, r *http.Request) { + fmt.Println("answerPost") + w.Header().Set("Content-Type", "application/json") + + lobby, player_id, err := validatePlayer(w, r) + if err != nil { + return + } + + lobby.mutex.Lock() + defer lobby.mutex.Unlock() + + var sdp webrtc.SessionDescription + + // Try to decode the request body into the struct. If there is an error, + // respond to the client with the error message and a 400 status code. + err = json.NewDecoder(r.Body).Decode(&sdp) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + lobby.Clients[player_id].Answer = &sdp + fmt.Printf("Lobby: %+v\n", lobby.Clients) +} + +func ice(w http.ResponseWriter, r *http.Request) { + // TODO: Implement the ice handler + w.Header().Set("Content-Type", "application/json") +} diff --git a/go.mod b/go.mod index b8711c42..12f425d8 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,19 @@ module github.com/pion/example-webrtc-applications/v3 -go 1.22 +go 1.23.1 -toolchain go1.23.6 +toolchain go1.24.1 require ( - github.com/asticode/go-astiav v0.19.0 + github.com/asticode/go-astiav v0.35.1 github.com/at-wat/ebml-go v0.17.1 - github.com/emiago/sipgo v0.29.0 - github.com/go-gst/go-gst v1.3.0 + github.com/ebitengine/debugui v0.1.0 + github.com/emiago/sipgo v0.30.0 + github.com/go-gst/go-gst v1.4.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/hajimehoshi/ebiten/v2 v2.8.7 + github.com/kelindar/binary v1.0.19 github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e github.com/pion/interceptor v0.1.37 github.com/pion/logging v0.2.3 @@ -24,15 +27,20 @@ require ( ) require ( - github.com/asticode/go-astikit v0.42.0 // indirect - github.com/go-gst/go-glib v1.3.0 // indirect + github.com/asticode/go-astikit v0.54.0 // indirect + github.com/ebitengine/gomobile v0.0.0-20250329061421-6d0a8e981e4c // indirect + github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/purego v0.8.2 // indirect + github.com/go-gst/go-glib v1.4.0 // indirect + github.com/go-text/typesetting v0.3.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.3.2 // indirect - github.com/icholy/digest v0.1.22 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/gobwas/ws v1.4.0 // indirect + github.com/hajimehoshi/bitmapfont/v3 v3.2.1 // indirect + github.com/icholy/digest v1.1.0 // indirect + github.com/jezek/xgb v1.1.1 // indirect github.com/mattn/go-pointer v0.0.1 // indirect + github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect github.com/pion/ice/v4 v4.0.10 // indirect @@ -43,8 +51,8 @@ require ( github.com/pion/stun/v3 v3.0.0 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect - github.com/rs/xid v1.5.0 // indirect - github.com/rs/zerolog v1.33.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.33.0 // indirect diff --git a/go.sum b/go.sum index c5cb66e2..70fb4261 100644 --- a/go.sum +++ b/go.sum @@ -1,48 +1,62 @@ -github.com/asticode/go-astiav v0.19.0 h1:tAyTiYCmwBuApfCZRBMdaOkyhfxN39ybvqXGZkw4OCk= -github.com/asticode/go-astiav v0.19.0/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s= -github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w= -github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= +github.com/asticode/go-astiav v0.35.1 h1:jq27Ihf+GXtOTnhzNTcpKrW1iLNRAuPSoarh7/SapYc= +github.com/asticode/go-astiav v0.35.1/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s= +github.com/asticode/go-astikit v0.54.0 h1:uq9eurgisdkYwJU9vSWIQaPH4MH0cac82sQH00kmSNQ= +github.com/asticode/go-astikit v0.54.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE= github.com/at-wat/ebml-go v0.17.1 h1:pWG1NOATCFu1hnlowCzrA1VR/3s8tPY6qpU+2FwW7X4= github.com/at-wat/ebml-go v0.17.1/go.mod h1:w1cJs7zmGsb5nnSvhWGKLCxvfu4FVx5ERvYDIalj1ww= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emiago/sipgo v0.29.0 h1:dg/FwwhSl6hQTiOTIHzcqemZm3tB7jvGQgIlJmuD2Nw= -github.com/emiago/sipgo v0.29.0/go.mod h1:ZQ/tl5t+3assyOjiKw/AInPkcawBJ2Or+d5buztOZsc= -github.com/go-gst/go-glib v1.3.0 h1:u+mPUdLmrDFA/MskIxInJY+M0O1RSkHeZYggnJGWlPk= -github.com/go-gst/go-glib v1.3.0/go.mod h1:JybIYeoHNwCkHGaBf1fHNIaM4sQTrJPkPLsi7dmPNOU= -github.com/go-gst/go-gst v1.3.0 h1:z4mQ7CNJXd6ZfkibzIT9kZKwtgEFJo7jJGlX9cXFzz0= -github.com/go-gst/go-gst v1.3.0/go.mod h1:2li6ghiCBz7/R6DA7itVto3gsYh0QKicwSxEefNVYqE= +github.com/ebitengine/debugui v0.1.0 h1:mnWmlsKSZ6ozRNTx2Bf1HI0gTEsaoV4G19P5WtS3IP0= +github.com/ebitengine/debugui v0.1.0/go.mod h1:wIKIq5RvNFb3+nFfJcYqSLvpC1ioD/BglWDtuWFSDz0= +github.com/ebitengine/gomobile v0.0.0-20250329061421-6d0a8e981e4c h1:Ccgks2VROTr6bIm1FFxG2jT6P1DaCBMj8g/O9xbOQ08= +github.com/ebitengine/gomobile v0.0.0-20250329061421-6d0a8e981e4c/go.mod h1:M6DDA2RbegvWBVv4Dq482lwyFTtMczT1A7UNm1qOYzY= +github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= +github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emiago/sipgo v0.30.0 h1:ZgA5jXZOGA2Xx4HqOXKGPTLbnwh7NIIqhfGA0EdFoIE= +github.com/emiago/sipgo v0.30.0/go.mod h1:DVr48FXa5HoRjYfSLDUKZ/1KZU4w23rBNtPMEyBmd3E= +github.com/go-gst/go-glib v1.4.0 h1:FB2uVfB0uqz7/M6EaDdWWlBZRQpvFAbWfL7drdw8lAE= +github.com/go-gst/go-glib v1.4.0/go.mod h1:GUIpWmkxQ1/eL+FYSjKpLDyTZx6Vgd9nNXt8dA31d5M= +github.com/go-gst/go-gst v1.4.0 h1:EikB43u4c3wc8d2RzlFRSfIGIXYzDy6Zls2vJqrG2BU= +github.com/go-gst/go-gst v1.4.0/go.mod h1:p8TLGtOxJLcrp6PCkTPdnanwWBxPZvYiHDbuSuwgO3c= +github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= +github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= +github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= -github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM= -github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc= +github.com/hajimehoshi/bitmapfont/v3 v3.2.1 h1:33Lw85DZolX3upouUqf6Qza8HYGIROvr7SYin7PzIZ8= +github.com/hajimehoshi/bitmapfont/v3 v3.2.1/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg= +github.com/hajimehoshi/ebiten/v2 v2.8.7 h1:DnvNZuB8RF0ffOUTuqaXHl9d51VAT9XYfEMQPYD37v4= +github.com/hajimehoshi/ebiten/v2 v2.8.7/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs= +github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= +github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= +github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= +github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= +github.com/kelindar/binary v1.0.19 h1:DNyQCtKjkLhBh9pnP49OWREddLB0Mho+1U/AOt/Qzxw= +github.com/kelindar/binary v1.0.19/go.mod h1:/twdz8gRLNMffx0U4UOgqm1LywPs6nd9YK2TX52MDh8= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e h1:L1QWI1FyFkgLOLSP/BlbkLiyLyqUuyxCCRJyULDinx8= github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e/go.mod h1:BN/Txse3qz8tZOmCm2OfajB2wHVujWmX3o9nVdsI6gE= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= +github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= @@ -79,19 +93,21 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gocv.io/x/gocv v0.40.0 h1:kGBu/UVj+dO6A9dhQmGOnCICSL7ke7b5YtX3R3azdXI= gocv.io/x/gocv v0.40.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -105,18 +121,47 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= From 571070c5c6171a2b3a868b0d95d2da9120bd1ceb Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Sun, 11 May 2025 08:01:47 +0300 Subject: [PATCH 2/7] Go tidy --- go.mod | 9 ++++++--- go.sum | 52 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 12f425d8..4b821189 100644 --- a/go.mod +++ b/go.mod @@ -21,8 +21,9 @@ require ( github.com/pion/rtp v1.8.15 github.com/pion/sdp/v3 v3.0.11 github.com/pion/webrtc/v4 v4.1.0 + github.com/rs/cors v1.11.1 gocv.io/x/gocv v0.40.0 - golang.org/x/image v0.23.0 + golang.org/x/image v0.25.0 golang.org/x/net v0.35.0 ) @@ -56,6 +57,8 @@ require ( github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.33.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index 70fb4261..a2f6bebf 100644 --- a/go.sum +++ b/go.sum @@ -89,8 +89,7 @@ github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= github.com/pion/webrtc/v4 v4.1.0 h1:yq/p0G5nKGbHISf0YKNA8Yk+kmijbblBvuSLwaJ4QYg= github.com/pion/webrtc/v4 v4.1.0/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -111,13 +110,30 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t gocv.io/x/gocv v0.40.0 h1:kGBu/UVj+dO6A9dhQmGOnCICSL7ke7b5YtX3R3azdXI= gocv.io/x/gocv v0.40.0/go.mod h1:zYdWMj29WAEznM3Y8NsU3A0TRq/wR/cy75jeUypThqU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= -golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= +golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= +golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -127,8 +143,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -139,8 +155,18 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -150,8 +176,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 814383eea0f34126db40fa2bc1fff170e1631850 Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Sun, 11 May 2025 08:23:30 +0300 Subject: [PATCH 3/7] Pre install hook, go 1.23 --- .github/.ci.conf | 23 ++++++++---- go.mod | 41 +++++++++++---------- go.sum | 93 ++++++++++++++++++++++++++++-------------------- 3 files changed, 94 insertions(+), 63 deletions(-) diff --git a/.github/.ci.conf b/.github/.ci.conf index 058e63fd..ed340ad4 100755 --- a/.github/.ci.conf +++ b/.github/.ci.conf @@ -1,17 +1,28 @@ # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT -PRE_TEST_HOOK=_install_gstreamer_hook -PRE_LINT_HOOK=_install_gstreamer_hook -GO_MOD_VERSION_EXPECTED=1.22 +PRE_TEST_HOOK=_install_dependencies_hook +PRE_LINT_HOOK=_install_dependencies_hook +GO_MOD_VERSION_EXPECTED=1.23 SKIP_i386_TESTS=true SKIP_API_DIFF=true function _install_gstreamer_hook(){ - set -e - - sudo apt-get update sudo apt-get purge -y libunwind-14-dev sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev sudo apt-get install -y libavcodec-dev libavutil-dev libavfilter-dev libswscale-dev libavformat-dev libavdevice-dev } + +function _install_ebiten_hook(){ + sudo apt-get install -y \ + libasound2-dev libgl1-mesa-dev libxcursor-dev libxi-dev \ + libxinerama-dev libxrandr-dev libxxf86vm-dev +} + +function _install_dependencies_hook(){ + set -e + + sudo apt-get update + _install_gstreamer_hook + _install_ebiten_hook +} diff --git a/go.mod b/go.mod index 4b821189..989b85ba 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,18 @@ module github.com/pion/example-webrtc-applications/v3 -go 1.23.1 +go 1.23.0 -toolchain go1.24.1 +toolchain go1.23.6 require ( - github.com/asticode/go-astiav v0.35.1 + github.com/asticode/go-astiav v0.19.0 github.com/at-wat/ebml-go v0.17.1 - github.com/ebitengine/debugui v0.1.0 - github.com/emiago/sipgo v0.30.0 - github.com/go-gst/go-gst v1.4.0 + github.com/ebitengine/debugui v0.1.1 + github.com/emiago/sipgo v0.29.0 + github.com/go-gst/go-gst v1.3.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 - github.com/hajimehoshi/ebiten/v2 v2.8.7 + github.com/hajimehoshi/ebiten/v2 v2.8.8 github.com/kelindar/binary v1.0.19 github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e github.com/pion/interceptor v0.1.37 @@ -23,25 +23,27 @@ require ( github.com/pion/webrtc/v4 v4.1.0 github.com/rs/cors v1.11.1 gocv.io/x/gocv v0.40.0 - golang.org/x/image v0.25.0 + golang.org/x/image v0.23.0 golang.org/x/net v0.35.0 ) require ( - github.com/asticode/go-astikit v0.54.0 // indirect - github.com/ebitengine/gomobile v0.0.0-20250329061421-6d0a8e981e4c // indirect + github.com/asticode/go-astikit v0.42.0 // indirect + github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect - github.com/ebitengine/purego v0.8.2 // indirect - github.com/go-gst/go-glib v1.4.0 // indirect - github.com/go-text/typesetting v0.3.0 // indirect + github.com/ebitengine/purego v0.8.0 // indirect + github.com/go-gst/go-glib v1.3.0 // indirect + github.com/go-text/typesetting v0.2.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.4.0 // indirect + github.com/gobwas/ws v1.3.2 // indirect github.com/hajimehoshi/bitmapfont/v3 v3.2.1 // indirect - github.com/icholy/digest v1.1.0 // indirect + github.com/icholy/digest v0.1.22 // indirect github.com/jezek/xgb v1.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-pointer v0.0.1 // indirect - github.com/pierrec/lz4/v4 v4.1.22 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pion/datachannel v1.5.10 // indirect github.com/pion/dtls/v3 v3.0.6 // indirect github.com/pion/ice/v4 v4.0.10 // indirect @@ -53,12 +55,13 @@ require ( github.com/pion/transport/v3 v3.0.7 // indirect github.com/pion/turn/v4 v4.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rs/xid v1.6.0 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/rs/zerolog v1.33.0 // indirect github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect github.com/wlynxg/anet v0.0.5 // indirect golang.org/x/crypto v0.33.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect golang.org/x/sync v0.12.0 // indirect golang.org/x/sys v0.31.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/text v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index a2f6bebf..b415797b 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,36 @@ -github.com/asticode/go-astiav v0.35.1 h1:jq27Ihf+GXtOTnhzNTcpKrW1iLNRAuPSoarh7/SapYc= -github.com/asticode/go-astiav v0.35.1/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s= -github.com/asticode/go-astikit v0.54.0 h1:uq9eurgisdkYwJU9vSWIQaPH4MH0cac82sQH00kmSNQ= -github.com/asticode/go-astikit v0.54.0/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE= +github.com/asticode/go-astiav v0.19.0 h1:tAyTiYCmwBuApfCZRBMdaOkyhfxN39ybvqXGZkw4OCk= +github.com/asticode/go-astiav v0.19.0/go.mod h1:K7D8UC6GeQt85FUxk2KVwYxHnotrxuEnp5evkkudc2s= +github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w= +github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/at-wat/ebml-go v0.17.1 h1:pWG1NOATCFu1hnlowCzrA1VR/3s8tPY6qpU+2FwW7X4= github.com/at-wat/ebml-go v0.17.1/go.mod h1:w1cJs7zmGsb5nnSvhWGKLCxvfu4FVx5ERvYDIalj1ww= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/ebitengine/debugui v0.1.0 h1:mnWmlsKSZ6ozRNTx2Bf1HI0gTEsaoV4G19P5WtS3IP0= -github.com/ebitengine/debugui v0.1.0/go.mod h1:wIKIq5RvNFb3+nFfJcYqSLvpC1ioD/BglWDtuWFSDz0= -github.com/ebitengine/gomobile v0.0.0-20250329061421-6d0a8e981e4c h1:Ccgks2VROTr6bIm1FFxG2jT6P1DaCBMj8g/O9xbOQ08= -github.com/ebitengine/gomobile v0.0.0-20250329061421-6d0a8e981e4c/go.mod h1:M6DDA2RbegvWBVv4Dq482lwyFTtMczT1A7UNm1qOYzY= +github.com/ebitengine/debugui v0.1.1 h1:3b4cIujNvKYhozlBjag9TWuuyo67Di3DgXWAz+f7xAY= +github.com/ebitengine/debugui v0.1.1/go.mod h1:wIKIq5RvNFb3+nFfJcYqSLvpC1ioD/BglWDtuWFSDz0= +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 h1:Gk1XUEttOk0/hb6Tq3WkmutWa0ZLhNn/6fc6XZpM7tM= +github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325/go.mod h1:ulhSQcbPioQrallSuIzF8l1NKQoD7xmMZc5NxzibUMY= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= -github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= -github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/emiago/sipgo v0.30.0 h1:ZgA5jXZOGA2Xx4HqOXKGPTLbnwh7NIIqhfGA0EdFoIE= -github.com/emiago/sipgo v0.30.0/go.mod h1:DVr48FXa5HoRjYfSLDUKZ/1KZU4w23rBNtPMEyBmd3E= -github.com/go-gst/go-glib v1.4.0 h1:FB2uVfB0uqz7/M6EaDdWWlBZRQpvFAbWfL7drdw8lAE= -github.com/go-gst/go-glib v1.4.0/go.mod h1:GUIpWmkxQ1/eL+FYSjKpLDyTZx6Vgd9nNXt8dA31d5M= -github.com/go-gst/go-gst v1.4.0 h1:EikB43u4c3wc8d2RzlFRSfIGIXYzDy6Zls2vJqrG2BU= -github.com/go-gst/go-gst v1.4.0/go.mod h1:p8TLGtOxJLcrp6PCkTPdnanwWBxPZvYiHDbuSuwgO3c= -github.com/go-text/typesetting v0.3.0 h1:OWCgYpp8njoxSRpwrdd1bQOxdjOXDj9Rqart9ML4iF4= -github.com/go-text/typesetting v0.3.0/go.mod h1:qjZLkhRgOEYMhU9eHBr3AR4sfnGJvOXNLt8yRAySFuY= -github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0= -github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE= +github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/emiago/sipgo v0.29.0 h1:dg/FwwhSl6hQTiOTIHzcqemZm3tB7jvGQgIlJmuD2Nw= +github.com/emiago/sipgo v0.29.0/go.mod h1:ZQ/tl5t+3assyOjiKw/AInPkcawBJ2Or+d5buztOZsc= +github.com/go-gst/go-glib v1.3.0 h1:u+mPUdLmrDFA/MskIxInJY+M0O1RSkHeZYggnJGWlPk= +github.com/go-gst/go-glib v1.3.0/go.mod h1:JybIYeoHNwCkHGaBf1fHNIaM4sQTrJPkPLsi7dmPNOU= +github.com/go-gst/go-gst v1.3.0 h1:z4mQ7CNJXd6ZfkibzIT9kZKwtgEFJo7jJGlX9cXFzz0= +github.com/go-gst/go-gst v1.3.0/go.mod h1:2li6ghiCBz7/R6DA7itVto3gsYh0QKicwSxEefNVYqE= +github.com/go-text/typesetting v0.2.0 h1:fbzsgbmk04KiWtE+c3ZD4W2nmCRzBqrqQOvYlwAOdho= +github.com/go-text/typesetting v0.2.0/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= -github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q= +github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -38,10 +39,10 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hajimehoshi/bitmapfont/v3 v3.2.1 h1:33Lw85DZolX3upouUqf6Qza8HYGIROvr7SYin7PzIZ8= github.com/hajimehoshi/bitmapfont/v3 v3.2.1/go.mod h1:8gLqGatKVu0pwcNCJguW3Igg9WQqVXF0zg/RvrGQWyg= -github.com/hajimehoshi/ebiten/v2 v2.8.7 h1:DnvNZuB8RF0ffOUTuqaXHl9d51VAT9XYfEMQPYD37v4= -github.com/hajimehoshi/ebiten/v2 v2.8.7/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs= -github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= -github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= +github.com/hajimehoshi/ebiten/v2 v2.8.8 h1:xyMxOAn52T1tQ+j3vdieZ7auDBOXmvjUprSrxaIbsi8= +github.com/hajimehoshi/ebiten/v2 v2.8.8/go.mod h1:durJ05+OYnio9b8q0sEtOgaNeBEQG7Yr7lRviAciYbs= +github.com/icholy/digest v0.1.22 h1:dRIwCjtAcXch57ei+F0HSb5hmprL873+q7PoVojdMzM= +github.com/icholy/digest v0.1.22/go.mod h1:uLAeDdWKIWNFMH0wqbwchbTQOmJWhzSnL7zmqSPqEEc= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/kelindar/binary v1.0.19 h1:DNyQCtKjkLhBh9pnP49OWREddLB0Mho+1U/AOt/Qzxw= @@ -50,13 +51,18 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e h1:L1QWI1FyFkgLOLSP/BlbkLiyLyqUuyxCCRJyULDinx8= github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e/go.mod h1:BN/Txse3qz8tZOmCm2OfajB2wHVujWmX3o9nVdsI6gE= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= -github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= @@ -90,6 +96,8 @@ github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGux github.com/pion/webrtc/v4 v4.1.0 h1:yq/p0G5nKGbHISf0YKNA8Yk+kmijbblBvuSLwaJ4QYg= github.com/pion/webrtc/v4 v4.1.0/go.mod h1:cgEGkcpxGkT6Di2ClBYO5lP9mFXbCfEOrkYUpjjCQO4= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -98,10 +106,13 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM= github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= @@ -116,16 +127,17 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= -golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ= -golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs= +golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= +golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -150,6 +162,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -157,6 +170,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= @@ -176,9 +191,10 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= @@ -189,5 +205,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= From 1cb22a6a919b66265dacf4a62bf9f986de342eca Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Sun, 11 May 2025 09:03:34 +0300 Subject: [PATCH 4/7] Add gopher license --- ebiten-game/game/gopher.png.license | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 ebiten-game/game/gopher.png.license diff --git a/ebiten-game/game/gopher.png.license b/ebiten-game/game/gopher.png.license new file mode 100644 index 00000000..71f804a7 --- /dev/null +++ b/ebiten-game/game/gopher.png.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2016 The Go Authors +SPDX-License-Identifier: CC-BY-3.0 \ No newline at end of file From bbdcb5def059d2089f420418166c88c111446df7 Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Sun, 11 May 2025 09:12:38 +0300 Subject: [PATCH 5/7] Add CC-BY-3.0 license --- LICENSES/CC-BY-3.0.txt | 319 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 LICENSES/CC-BY-3.0.txt diff --git a/LICENSES/CC-BY-3.0.txt b/LICENSES/CC-BY-3.0.txt new file mode 100644 index 00000000..6f7f096d --- /dev/null +++ b/LICENSES/CC-BY-3.0.txt @@ -0,0 +1,319 @@ +Creative Commons Legal Code + +Attribution 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(f) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined above) for the purposes of this + License. + c. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + d. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + e. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + f. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + g. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + h. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + i. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor waives the + exclusive right to collect such royalties for any exercise by You + of the rights granted under this License; and, + iii. Voluntary License Schemes. The Licensor waives the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved. + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(b), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(b), as requested. + b. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and (iv) , consistent with Section 3(b), in the case of an Adaptation, + a credit identifying the use of the Work in the Adaptation (e.g., + "French translation of the Work by Original Author," or "Screenplay + based on original Work by Original Author"). The credit required by + this Section 4 (b) may be implemented in any reasonable manner; + provided, however, that in the case of a Adaptation or Collection, at + a minimum such credit will appear, if a credit for all contributing + authors of the Adaptation or Collection appears, then as part of these + credits and in a manner at least as prominent as the credits for the + other contributing authors. For the avoidance of doubt, You may only + use the credit required by this Section for the purpose of attribution + in the manner set out above and, by exercising Your rights under this + License, You may not implicitly or explicitly assert or imply any + connection with, sponsorship or endorsement by the Original Author, + Licensor and/or Attribution Parties, as appropriate, of You or Your + use of the Work, without the separate, express prior written + permission of the Original Author, Licensor and/or Attribution + Parties. + c. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING, LICENSOR +OFFERS THE WORK AS-IS AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY +KIND CONCERNING THE WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, +INCLUDING, WITHOUT LIMITATION, WARRANTIES OF TITLE, MERCHANTIBILITY, +FITNESS FOR A PARTICULAR PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF +LATENT OR OTHER DEFECTS, ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, +WHETHER OR NOT DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION +OF IMPLIED WARRANTIES, SO SUCH EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of this License. + + Creative Commons may be contacted at https://creativecommons.org/. \ No newline at end of file From af2940ee30d6332c362d24020e669f8a6931a3a1 Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Sun, 11 May 2025 09:15:37 +0300 Subject: [PATCH 6/7] Go tidy --- go.sum | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.sum b/go.sum index b415797b..8ffbce3b 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/go-gst/go-gst v1.3.0 h1:z4mQ7CNJXd6ZfkibzIT9kZKwtgEFJo7jJGlX9cXFzz0= github.com/go-gst/go-gst v1.3.0/go.mod h1:2li6ghiCBz7/R6DA7itVto3gsYh0QKicwSxEefNVYqE= github.com/go-text/typesetting v0.2.0 h1:fbzsgbmk04KiWtE+c3ZD4W2nmCRzBqrqQOvYlwAOdho= github.com/go-text/typesetting v0.2.0/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I= +github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY= +github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= @@ -170,8 +172,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= From 4c8c049a6e33e837841ef4bb65e241b5773642df Mon Sep 17 00:00:00 2001 From: Joe Turki Date: Sun, 11 May 2025 18:52:37 +0300 Subject: [PATCH 7/7] Fix lint errors --- ebiten-game/game/main.go | 437 ++++++++++++++------------- ebiten-game/game/ui.go | 16 +- ebiten-game/signaling-server/main.go | 313 ++++++++++++------- 3 files changed, 430 insertions(+), 336 deletions(-) diff --git a/ebiten-game/game/main.go b/ebiten-game/game/main.go index b2143745..b28285af 100644 --- a/ebiten-game/game/main.go +++ b/ebiten-game/game/main.go @@ -10,10 +10,6 @@ import ( "net/http" "strconv" - //"runtime" - - //"github.com/pion/randutil" - _ "image/jpeg" _ "image/png" "io" @@ -27,19 +23,18 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/kelindar/binary" - //"github.com/hajimehoshi/ebiten/v2/inpututil" ) var img *ebiten.Image var ( - pos_x = 40.0 - pos_y = 40.0 - remote_pos_x = 40.0 - remote_pos_y = 40.0 + posX = 40.0 + posY = 40.0 + remotePosX = 40.0 + remotePosY = 40.0 ) -var lobby_id string +var lobbyID string var signalingIP = "127.0.0.1" var port = 3000 @@ -48,10 +43,10 @@ func getSignalingURL() string { return "http://" + signalingIP + ":" + strconv.Itoa(port) } -// players registered by host -var registered_players = make(map[int]struct{}) +// players registered by host. +var registeredPlayers = make(map[int]struct{}) -// client to the HTTP signaling server +// client to the HTTP signaling server. var httpClient = &http.Client{ Timeout: 10 * time.Second, } @@ -64,8 +59,8 @@ func init() { } } -// implements ebiten.Game interface -type Game struct { +// implements ebiten.game interface. +type game struct { debugUI debugui.DebugUI inputCapturingState debugui.InputCapturingState @@ -73,46 +68,46 @@ type Game struct { logSubmitBuf string logUpdated bool - lobby_id string - isHost bool + lobbyID string + isHost bool localDebugInformation string remoteDebugInformation string } -func NewGame() (*Game, error) { - g := &Game{} +func NewGame() (*game, error) { + g := &game{} return g, nil } // Layout implements Game. -func (g *Game) Layout(outsideWidth int, outsideHeight int) (int, int) { +func (g *game) Layout(outsideWidth int, outsideHeight int) (int, int) { return outsideWidth, outsideHeight } // called every tick (default 60 times a second) -// updates game logical state -func (g *Game) Update() error { - +// updates game logical state. +func (g *game) Update() error { if ebiten.IsKeyPressed(ebiten.KeyUp) { - pos_y -= 1 + posY-- } if ebiten.IsKeyPressed(ebiten.KeyDown) { - pos_y += 1 + posY++ } if ebiten.IsKeyPressed(ebiten.KeyLeft) { - pos_x -= 1 + posX-- } if ebiten.IsKeyPressed(ebiten.KeyRight) { - pos_x += 1 + posX++ } inputCaptured, err := g.debugUI.Update(func(ctx *debugui.Context) error { g.logWindow(ctx) + return nil }) if err != nil { @@ -123,8 +118,8 @@ func (g *Game) Update() error { } // called every frame, depends on the monitor refresh rate -// which will probably be at least 60 times per second -func (g *Game) Draw(screen *ebiten.Image) { +// which will probably be at least 60 times per second. +func (g *game) Draw(screen *ebiten.Image) { // prints something on the screen debugString := fmt.Sprintf("FPS: %f", ebiten.ActualFPS()) debugString += "\n" + g.localDebugInformation + "\n" + g.remoteDebugInformation @@ -132,43 +127,43 @@ func (g *Game) Draw(screen *ebiten.Image) { // draw image op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(pos_x, pos_y) + op.GeoM.Translate(posX, posY) screen.DrawImage(img, op) // draw remote op2 := &ebiten.DrawImageOptions{} - op2.GeoM.Translate(remote_pos_x, remote_pos_y) + op2.GeoM.Translate(remotePosX, remotePosY) screen.DrawImage(img, op2) g.debugUI.Draw(screen) } var ( - // probably move all webrtc networking stuff to a struct i can manage + // probably move all webrtc networking stuff to a struct i can manage. peerConnection *webrtc.PeerConnection ) const messageSize = 32 -type PlayerData struct { +type playerData struct { Id int } -func (game *Game) startConnection() { +func (g *game) startConnection() { // Since this behavior diverges from the WebRTC API it has to be // enabled using a settings engine. Mixing both detached and the // OnMessage DataChannel API is not supported. - // Create a SettingEngine and enable Detach + // Create a SettingEngine and enable Detach. s := webrtc.SettingEngine{} s.DetachDataChannels() - // Create an API object with the engine + // Create an API object with the engine. api := webrtc.NewAPI(webrtc.WithSettingEngine(s)) // Everything below is the Pion WebRTC API! Thanks for using it ❤️. - // Prepare the configuration + // Prepare the configuration. config := webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { @@ -177,295 +172,300 @@ func (game *Game) startConnection() { }, } - // Create a new RTCPeerConnection using the API object + // Create a new RTCPeerConnection using the API object. pc, err := api.NewPeerConnection(config) if err != nil { panic(err) } - // Set the global variable to the newly created RTCPeerConnection + // Set the global variable to the newly created RTCPeerConnection. peerConnection = pc - // Set the handler for Peer connection state - // This will notify you when the peer has connected/disconnected + // Set the handler for Peer connection state. + // This will notify you when the peer has connected/disconnected. peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { - game.writeLog(fmt.Sprintf("Peer Connection State has changed: %s\n", s.String())) + g.writeLog(fmt.Sprintf("Peer Connection State has changed: %s\n", s.String())) if s == webrtc.PeerConnectionStateFailed { - // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. + // Wait until PeerConnection has had no network activity for 30 seconds or another failure. + // It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. - game.writeLog(fmt.Sprintln("Peer Connection has gone to failed exiting")) + g.writeLog(fmt.Sprintln("Peer Connection has gone to failed exiting")) os.Exit(0) } if s == webrtc.PeerConnectionStateClosed { // PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify - game.writeLog(fmt.Sprintln("Peer Connection has gone to closed exiting")) + g.writeLog(fmt.Sprintln("Peer Connection has gone to closed exiting")) os.Exit(0) } }) // Set the handler for ICE connection state - // This will notify you when the peer has connected/disconnected + // This will notify you when the peer has connected/disconnected. peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { - game.writeLog(fmt.Sprintf("ICE Connection State has changed: %s\n", connectionState.String())) + g.writeLog(fmt.Sprintf("ICE Connection State has changed: %s\n", connectionState.String())) }) - // the one that gives the answer is the host - if game.isHost { - game.writeLog("Hosting a lobby") - // Host creates lobby - lobby_resp, err := httpClient.Get(getSignalingURL() + "/lobby/host") + // the one that gives the answer is the host. + if g.isHost { //nolint:nestif + g.writeLog("Hosting a lobby") + // Host creates lobby. + lobbyResp, err := httpClient.Get(getSignalingURL() + "/lobby/host") if err != nil { panic(err) } - bodyBytes, err := io.ReadAll(lobby_resp.Body) + bodyBytes, err := io.ReadAll(lobbyResp.Body) if err != nil { panic(err) } - lobby_id = string(bodyBytes) - lobby_id_str := fmt.Sprintf("Lobby ID: %s\n", lobby_id) - game.writeLog(lobby_id_str) + lobbyID = string(bodyBytes) + lobbyIDStr := fmt.Sprintf("Lobby ID: %s\n", lobbyID) + g.writeLog(lobbyIDStr) - // Register data channel creation handling + // Register data channel creation handling. peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { - game.writeLog(fmt.Sprintf("New DataChannel %s %d\n", d.Label(), d.ID())) + g.writeLog(fmt.Sprintf("New DataChannel %s %d\n", d.Label(), d.ID())) - // Register channel opening handling + // Register channel opening handling. d.OnOpen(func() { - s := fmt.Sprintf("Data channel '%s'-'%d' open on host side!", d.Label(), d.ID()) - game.writeLog(s) + g.writeLog(s) - // Detach the data channel + // Detach the data channel. raw, dErr := d.Detach() if dErr != nil { panic(dErr) } - // Handle reading from the data channel - go ReadLoop(game, raw) + // Handle reading from the data channel. + go ReadLoop(g, raw) - // Handle writing to the data channel - go WriteLoop(game, raw) + // Handle writing to the data channel. + go WriteLoop(g, raw) }) }) - // poll for offer from signaling server for player - pollForPlayerOffer := func(player_id int) { + // poll for offer from signaling server for player. + pollForPlayerOffer := func(playerID int) { ticker := time.NewTicker(1 * time.Second) - for { - select { - case t := <-ticker.C: - game.writeLog(fmt.Sprintln("Tick at", t)) - game.writeLog(fmt.Sprintf("Polling for offer for %d\n", player_id)) - // hardcode that there is only one other player and they have player_id 1 - getUrl := getSignalingURL() + "/offer/get?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_id) - game.writeLog(fmt.Sprintln(getUrl)) - offer_resp, err := httpClient.Get(getUrl) - if err != nil { - panic(err) - } - if offer_resp.StatusCode != http.StatusOK { - continue - } - body := new(bytes.Buffer) - body.ReadFrom(offer_resp.Body) - game.writeLog(fmt.Sprintf("Got offer %v\n", body.String())) - offer := webrtc.SessionDescription{} - err = json.NewDecoder(body).Decode(&offer) - if err != nil { - panic(err) - } - // Set the remote SessionDescription - err = peerConnection.SetRemoteDescription(offer) - if err != nil { - panic(err) - } - // Create answer - answer, err := peerConnection.CreateAnswer(nil) - if err != nil { - panic(err) - } + for range ticker.C { + g.writeLog(fmt.Sprintf("Polling for offer for %d\n", playerID)) + // hardcode that there is only one other player and they have player_id 1. + getUrl := getSignalingURL() + "/offer/get?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerID) + g.writeLog(fmt.Sprintln(getUrl)) + offerResp, err := httpClient.Get(getUrl) + if err != nil { + panic(err) + } + if offerResp.StatusCode != http.StatusOK { + continue + } + body := new(bytes.Buffer) + _, err = body.ReadFrom(offerResp.Body) + if err != nil { + panic(err) + } + + g.writeLog(fmt.Sprintf("Got offer %v\n", body.String())) + offer := webrtc.SessionDescription{} + err = json.NewDecoder(body).Decode(&offer) + if err != nil { + panic(err) + } + // Set the remote SessionDescription. + err = peerConnection.SetRemoteDescription(offer) + if err != nil { + panic(err) + } + // Create answer. + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } - // Create channel that is blocked until ICE Gathering is complete - gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + // Create channel that is blocked until ICE Gathering is complete. + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) - // Sets the LocalDescription, and starts our UDP listeners - err = peerConnection.SetLocalDescription(answer) - if err != nil { - panic(err) - } + // Sets the LocalDescription, and starts our UDP listeners. + err = peerConnection.SetLocalDescription(answer) + if err != nil { + panic(err) + } - // Block until ICE Gathering is complete, disabling trickle ICE - // we do this because we only can exchange one signaling message - // in a production application you should exchange ICE Candidates via OnICECandidate - <-gatherComplete - // send answer we generated to the signaling server - answerJson, err := json.Marshal(peerConnection.LocalDescription()) - if err != nil { - panic(err) - } - postUrl := getSignalingURL() + "/answer/post?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_id) - game.writeLog(fmt.Sprintln(postUrl)) - httpClient.Post(postUrl, "application/json", bytes.NewBuffer(answerJson)) - // if we have successfully set the remote description, we can break out of the loop - ticker.Stop() - return + // Block until ICE Gathering is complete, disabling trickle ICE. + // we do this because we only can exchange one signaling message + // in a production application you should exchange ICE Candidates via OnICECandidate. + <-gatherComplete + // send answer we generated to the signaling server. + answerJson, err := json.Marshal(peerConnection.LocalDescription()) + if err != nil { + panic(err) } + postUrl := getSignalingURL() + "/answer/post?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerID) + g.writeLog(fmt.Sprintln(postUrl)) + _, err = httpClient.Post(postUrl, "application/json", bytes.NewBuffer(answerJson)) + if err != nil { + panic(err) + } + + // if we have successfully set the remote description, we can break out of the loop. + ticker.Stop() + + return } } go func() { ticker := time.NewTicker(1 * time.Second) - for { - select { - case t := <-ticker.C: - game.writeLog(fmt.Sprintln("Polling for lobby ID {", lobby_id, "} at", t)) - idUrl := getSignalingURL() + "/lobby/unregisteredPlayers?id=" + lobby_id - game.writeLog(fmt.Sprintln(idUrl)) - id_resp, err := httpClient.Get(idUrl) - if err != nil { - panic(err) - } - if id_resp.StatusCode != http.StatusOK { - continue - } - var player_ids []int - err = json.NewDecoder(id_resp.Body).Decode(&player_ids) - if err != nil { - panic(err) - } - game.writeLog(fmt.Sprintf("Player IDs: %v\n", player_ids)) - // poll for all of the unregistered players - for _, player_id := range player_ids { - // only start goroutine if player_id hasn't been registered yet - if _, ok := registered_players[player_id]; !ok { - registered_players[player_id] = struct{}{} - go pollForPlayerOffer(player_id) - } + for t := range ticker.C { + g.writeLog(fmt.Sprintln("Polling for lobby ID {", lobbyID, "} at", t)) + idUrl := getSignalingURL() + "/lobby/unregisteredPlayers?id=" + lobbyID + g.writeLog(fmt.Sprintln(idUrl)) + idResp, err := httpClient.Get(idUrl) + if err != nil { + panic(err) + } + if idResp.StatusCode != http.StatusOK { + continue + } + var playerIds []int + err = json.NewDecoder(idResp.Body).Decode(&playerIds) + if err != nil { + panic(err) + } + g.writeLog(fmt.Sprintf("Player IDs: %v\n", playerIds)) + // poll for all of the unregistered players. + for _, playerID := range playerIds { + // only start goroutine if playerID hasn't been registered yet. + if _, ok := registeredPlayers[playerID]; !ok { + registeredPlayers[playerID] = struct{}{} + go pollForPlayerOffer(playerID) } } } }() } else { - game.writeLog("Joining lobby: " + lobby_id) - // the following is for the client joining the lobby - // get lobby id from text input - lobby_id = game.lobby_id - response, err := httpClient.Get(getSignalingURL() + "/lobby/join?id=" + lobby_id) + g.writeLog("Joining lobby: " + lobbyID) + // the following is for the client joining the lobby. + // get lobby id from text input. + lobbyID = g.lobbyID + response, err := httpClient.Get(getSignalingURL() + "/lobby/join?id=" + lobbyID) if err != nil { panic(err) } - var player_data PlayerData - err = json.NewDecoder(response.Body).Decode(&player_data) + var playerData playerData + err = json.NewDecoder(response.Body).Decode(&playerData) if err != nil { panic(err) } - game.writeLog(fmt.Sprintf("Player ID: %v\n", player_data)) - // Create a datachannel with label 'data' + g.writeLog(fmt.Sprintf("Player ID: %v\n", playerData)) + // Create a datachannel with label 'data'. dataChannel, err := peerConnection.CreateDataChannel("data", nil) if err != nil { panic(err) } - // Register channel opening handling + // Register channel opening handling. dataChannel.OnOpen(func() { s := fmt.Sprintf("Data channel '%s'-'%d' open on client side!", dataChannel.Label(), dataChannel.ID()) - game.writeLog(s) + g.writeLog(s) - // Detach the data channel + // Detach the data channel. raw, dErr := dataChannel.Detach() if dErr != nil { panic(dErr) } - // Handle reading from the data channel - go ReadLoop(game, raw) + // Handle reading from the data channel. + go ReadLoop(g, raw) - // Handle writing to the data channel - go WriteLoop(game, raw) + // Handle writing to the data channel. + go WriteLoop(g, raw) }) - // Create an offer to send to the browser + // Create an offer to send to the browser. offer, err := peerConnection.CreateOffer(nil) if err != nil { panic(err) } - // Sets the LocalDescription, and starts our UDP listeners + // Sets the LocalDescription, and starts our UDP listeners. err = peerConnection.SetLocalDescription(offer) if err != nil { panic(err) } - // print out possible offers from different ICE Candidates + // print out possible offers from different ICE Candidates. peerConnection.OnICECandidate(func(candidate *webrtc.ICECandidate) { if candidate != nil { offerJson, err := json.Marshal(peerConnection.LocalDescription()) if err != nil { panic(err) } - postUrl := getSignalingURL() + "/offer/post?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_data.Id) - game.writeLog(fmt.Sprintln(postUrl)) - httpClient.Post(postUrl, "application/json", bytes.NewBuffer(offerJson)) + postUrl := getSignalingURL() + "/offer/post?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerData.Id) + g.writeLog(fmt.Sprintln(postUrl)) + _, err = httpClient.Post(postUrl, "application/json", bytes.NewBuffer(offerJson)) + if err != nil { + panic(err) + } } }) answer := webrtc.SessionDescription{} - // read answer from other peer (wait till we actually get something) + // read answer from other peer (wait till we actually get something). ticker := time.NewTicker(1 * time.Second) go func() { - for { - select { - case t := <-ticker.C: - game.writeLog(fmt.Sprintln("Tick at", t)) - game.writeLog(fmt.Sprintln("Polling for answer")) - url := getSignalingURL() + "/answer/get?lobby_id=" + lobby_id + "&player_id=" + strconv.Itoa(player_data.Id) - fmt.Println(url) - answer_resp, err := httpClient.Get(url) - if err != nil { - panic(err) - } - if answer_resp.StatusCode != http.StatusOK { - continue - } - body := new(bytes.Buffer) - body.ReadFrom(answer_resp.Body) - game.writeLog(fmt.Sprintf("Got answer %v\n", body.String())) - err = json.NewDecoder(body).Decode(&answer) - if err != nil { - panic(err) - } - - if err := peerConnection.SetRemoteDescription(answer); err != nil { - panic(err) - } + for range ticker.C { + g.writeLog(fmt.Sprintln("Polling for answer")) + url := getSignalingURL() + "/answer/get?lobby_id=" + lobbyID + "&player_id=" + strconv.Itoa(playerData.Id) + fmt.Println(url) + answerResp, err := httpClient.Get(url) + if err != nil { + panic(err) + } + if answerResp.StatusCode != http.StatusOK { + continue + } + body := new(bytes.Buffer) + body.ReadFrom(answerResp.Body) + g.writeLog(fmt.Sprintf("Got answer %v\n", body.String())) + err = json.NewDecoder(body).Decode(&answer) + if err != nil { + panic(err) + } - // if we have successfully set the remote description, we can break out of the loop - ticker.Stop() - return + if err := peerConnection.SetRemoteDescription(answer); err != nil { + panic(err) } + + // if we have successfully set the remote description, we can break out of the loop. + ticker.Stop() + + return } }() } } -func (g *Game) closeConnection() { +func (g *game) closeConnection() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } - // TODO: this doesn't work, fix this + // this doesn't work, fix this. if g.isHost { - // delete lobby if host + // delete lobby if host. url := getSignalingURL() + "/lobby/delete" fmt.Println(url) - httpClient.Get(url) + _, err := httpClient.Get(url) + if err != nil { + panic(err) + } } } -// entry point of the program +// entry point of the program. func main() { ebiten.SetWindowSize(640, 480) ebiten.SetWindowTitle("Hello, World!") @@ -481,22 +481,23 @@ func main() { os.Exit(1) } - // close the connection when the game ends + // close the connection when the game ends. g.closeConnection() } type Packet struct { - Pos_x float64 - Pos_y float64 + PosX float64 + PosY float64 } -// ReadLoop shows how to read from the datachannel directly -func ReadLoop(g *Game, d io.Reader) { +// ReadLoop shows how to read from the datachannel directly. +func ReadLoop(game *game, d io.Reader) { for { buffer := make([]byte, messageSize) _, err := io.ReadFull(d, buffer) if err != nil { - g.writeLog(fmt.Sprintln("Datachannel closed; Exit the readloop:", err)) + game.writeLog(fmt.Sprintln("Datachannel closed; Exit the readloop:", err)) + return } @@ -506,20 +507,20 @@ func ReadLoop(g *Game, d io.Reader) { panic(err) } - remote_pos_x = packet.Pos_x - remote_pos_y = packet.Pos_y + remotePosX = packet.PosX + remotePosY = packet.PosY - g.remoteDebugInformation = fmt.Sprintf("Message from DataChannel: %f %f", packet.Pos_x, packet.Pos_y) + game.remoteDebugInformation = fmt.Sprintf("Message from DataChannel: %f %f", packet.PosX, packet.PosY) } } -// WriteLoop shows how to write to the datachannel directly -func WriteLoop(g *Game, d io.Writer) { +// WriteLoop shows how to write to the datachannel directly. +func WriteLoop(g *game, d io.Writer) { ticker := time.NewTicker(time.Millisecond * 20) defer ticker.Stop() for range ticker.C { - packet := &Packet{pos_x, pos_y} - g.localDebugInformation = fmt.Sprintf("Sending x:%f y:%f", packet.Pos_x, packet.Pos_y) + packet := &Packet{posX, posY} + g.localDebugInformation = fmt.Sprintf("Sending x:%f y:%f", packet.PosX, packet.PosY) encoded, err := binary.Marshal(packet) if err != nil { panic(err) diff --git a/ebiten-game/game/ui.go b/ebiten-game/game/ui.go index 2dfd9486..ad2e94ce 100644 --- a/ebiten-game/game/ui.go +++ b/ebiten-game/game/ui.go @@ -10,7 +10,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -func (g *Game) writeLog(text string) { +func (g *game) writeLog(text string) { if len(g.logBuf) > 0 { g.logBuf += "\n" } @@ -18,7 +18,7 @@ func (g *Game) writeLog(text string) { g.logUpdated = true } -func (g *Game) logWindow(ctx *debugui.Context) { +func (g *game) logWindow(ctx *debugui.Context) { ctx.Window("Log Window", image.Rect(350, 40, 650, 290), func(layout debugui.ContainerLayout) { ctx.SetGridLayout([]int{-1}, []int{-1, 0}) ctx.Panel(func(layout debugui.ContainerLayout) { @@ -30,17 +30,17 @@ func (g *Game) logWindow(ctx *debugui.Context) { } }) ctx.GridCell(func(bounds image.Rectangle) { - submit_open := func() { + submitOpen := func() { g.isHost = true g.startConnection() } - submit_join := func() { + submitJoin := func() { g.isHost = false if g.logSubmitBuf == "" { return } - g.lobby_id = g.logSubmitBuf + g.lobbyID = g.logSubmitBuf g.logSubmitBuf = "" g.startConnection() } @@ -49,15 +49,15 @@ func (g *Game) logWindow(ctx *debugui.Context) { ctx.Text("Lobby ID:") ctx.TextField(&g.logSubmitBuf).On(func() { if ebiten.IsKeyPressed(ebiten.KeyEnter) { - submit_join() + submitJoin() ctx.SetTextFieldValue(g.logSubmitBuf) } }) ctx.Button("Open").On(func() { - submit_open() + submitOpen() }) ctx.Button("Join").On(func() { - submit_join() + submitJoin() }) }) }) diff --git a/ebiten-game/signaling-server/main.go b/ebiten-game/signaling-server/main.go index 8dc45dce..e0df3255 100644 --- a/ebiten-game/signaling-server/main.go +++ b/ebiten-game/signaling-server/main.go @@ -17,61 +17,69 @@ import ( "github.com/rs/cors" ) -type ClientConnection struct { +type clientConnection struct { IsHost bool Offer *webrtc.SessionDescription Answer *webrtc.SessionDescription } -type Lobby struct { +type lobby struct { mutex sync.Mutex // host is first client in lobby.Clients - Clients []ClientConnection + Clients []clientConnection } -var lobby_list = map[string]*Lobby{} +var lobbyList = map[string]*lobby{} -type PlayerData struct { +type playerData struct { // player id is index in lobby.Clients Id int } -var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") +var ( + errLobbyNotFound = errors.New("lobby not found") + errPlayerNotFound = errors.New("player not found") +) + +func generateNewLobbyID() string { + letters := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") -func generateNewLobbyId() string { // have random size for lobby id size := 6 buffer := make([]rune, size) for i := range buffer { - buffer[i] = letters[rand.Intn(len(letters))] + buffer[i] = letters[rand.Intn(len(letters))] //nolint:gosec } id := string(buffer) // check if room id is already in lobby_list - _, ok := lobby_list[id] + _, ok := lobbyList[id] if ok { // if it already exists, call function again - return generateNewLobbyId() + return generateNewLobbyID() } + return id } func makeLobby() string { - lobby := Lobby{} - lobby.Clients = []ClientConnection{} + lobby := lobby{} + lobby.Clients = []clientConnection{} // first client is always host - lobby_id := generateNewLobbyId() - lobby_list[lobby_id] = &lobby - return lobby_id + lobbyID := generateNewLobbyID() + lobbyList[lobbyID] = &lobby + + return lobbyID } -func getLobbyIds() []string { - lobbies := make([]string, len(lobby_list)) +func getLobbyIDs() []string { + lobbies := make([]string, len(lobbyList)) i := 0 - for k := range lobby_list { + for k := range lobbyList { lobbies[i] = k i++ } + return lobbies } @@ -93,167 +101,235 @@ func main() { // all origins accepted with simple methods (GET, POST). See // documentation below for more options. handler := cors.Default().Handler(mux) - http.ListenAndServe(":3000", handler) + err := http.ListenAndServe(":3000", handler) //nolint:gosec + if err != nil { + fmt.Printf("Failed to start server: %s", err) + + return + } } -func lobbyHost(w http.ResponseWriter, r *http.Request) { - lobby_id := makeLobby() - lobby := lobby_list[lobby_id] +func lobbyHost(res http.ResponseWriter, _ *http.Request) { + lobbyID := makeLobby() + lobby := lobbyList[lobbyID] lobby.mutex.Lock() defer lobby.mutex.Unlock() // host is first client in lobby.Clients - lobby.Clients = append(lobby.Clients, ClientConnection{IsHost: true}) + lobby.Clients = append(lobby.Clients, clientConnection{IsHost: true}) // return lobby id to host - io.Writer.Write(w, []byte(lobby_id)) + _, err := io.Writer.Write(res, []byte(lobbyID)) + if err != nil { + fmt.Printf("Failed to write lobby_id: %s", err) + + return + } fmt.Println("lobbyHost") - fmt.Printf("lobby added: %s\n", lobby_id) + fmt.Printf("lobby added: %s\n", lobbyID) // print all lobbies - fmt.Printf("lobby_list:%s\n", getLobbyIds()) + fmt.Printf("lobby_list:%s\n", getLobbyIDs()) } -// call "/lobby?id={lobby_id}" to connect to lobby -func lobbyJoin(w http.ResponseWriter, r *http.Request) { +// call "/lobby?id={lobby_id}" to connect to lobby. +func lobbyJoin(res http.ResponseWriter, req *http.Request) { fmt.Println("lobbyJoin") - w.Header().Set("Content-Type", "application/json") + res.Header().Set("Content-Type", "application/json") // https://freshman.tech/snippets/go/extract-url-query-params/ // get lobby id from query params - lobby_id := r.URL.Query().Get("id") - fmt.Printf("lobby_id: %s\n", lobby_id) + lobbyID := req.URL.Query().Get("id") + fmt.Printf("lobby_id: %s\n", lobbyID) // only continue with connection if lobby exists - lobby, ok := lobby_list[lobby_id] + lobby, ok := lobbyList[lobbyID] // If the key doesn't exist, return error if !ok { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 - Lobby not found")) + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte("404 - Lobby not found")) + if err != nil { + fmt.Printf("Failed to write lobby_not_found: %s", err) + + return + } + return } lobby.mutex.Lock() defer lobby.mutex.Unlock() - body, err := io.ReadAll(r.Body) + body, err := io.ReadAll(req.Body) if err != nil { fmt.Printf("Failed to read body: %s", err) + return } fmt.Printf("body: %s", body) // send player id once generated - lobby.Clients = append(lobby.Clients, ClientConnection{IsHost: false}) + lobby.Clients = append(lobby.Clients, clientConnection{IsHost: false}) // player id is index in lobby.Clients - player_id := len(lobby.Clients) - 1 - fmt.Printf("player_id: %d\n", player_id) + playerID := len(lobby.Clients) - 1 + fmt.Printf("player_id: %d\n", playerID) fmt.Println(lobby.Clients) - player_data := PlayerData{Id: player_id} - jsonValue, _ := json.Marshal(player_data) - io.Writer.Write(w, jsonValue) + playerData := playerData{Id: playerID} + + jsonValue, err := json.Marshal(playerData) + if err != nil { + fmt.Printf("Failed to marshal player_data: %s", err) + + return + } + + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write player_data: %s", err) + + return + } } -func lobbyDelete(w http.ResponseWriter, r *http.Request) { +func lobbyDelete(res http.ResponseWriter, req *http.Request) { fmt.Println("lobbyDelete") - w.Header().Set("Content-Type", "application/json") + res.Header().Set("Content-Type", "application/json") // https://freshman.tech/snippets/go/extract-url-query-params/ // get lobby id from query params - lobby_id := r.URL.Query().Get("id") - fmt.Printf("lobby_id: %s\n", lobby_id) + lobbyID := req.URL.Query().Get("id") + fmt.Printf("lobby_id: %s\n", lobbyID) // delete lobby - delete(lobby_list, lobby_id) - fmt.Printf("lobby_list:%s\n", getLobbyIds()) + delete(lobbyList, lobbyID) + fmt.Printf("lobby_list:%s\n", getLobbyIDs()) } -// return players who haven't been registered yet by the host -func lobbyUnregisteredPlayers(w http.ResponseWriter, r *http.Request) { +// return players who haven't been registered yet by the host. +func lobbyUnregisteredPlayers(res http.ResponseWriter, req *http.Request) { fmt.Println("UnregisteredPlayers") - w.Header().Set("Content-Type", "application/json") + res.Header().Set("Content-Type", "application/json") // https://freshman.tech/snippets/go/extract-url-query-params/ // get lobby id from query params - lobby_id := r.URL.Query().Get("id") - lobby := lobby_list[lobby_id] + lobbyID := req.URL.Query().Get("id") + lobby := lobbyList[lobbyID] lobby.mutex.Lock() defer lobby.mutex.Unlock() // get all players who haven't been registered yet - player_ids := []int{} + playerIDs := []int{} for i, client := range lobby.Clients { if !client.IsHost && client.Answer == nil { - player_ids = append(player_ids, i) + playerIDs = append(playerIDs, i) } } // return lobby id to host - jsonValue, _ := json.Marshal(player_ids) - io.Writer.Write(w, jsonValue) - fmt.Printf("player_ids %v\n", player_ids) + jsonValue, err := json.Marshal(playerIDs) + if err != nil { + fmt.Printf("Failed to marshal player_ids: %s", err) + + return + } + + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write player_ids: %s", err) + + return + } + + fmt.Printf("player_ids %v\n", playerIDs) } -func validatePlayer(w http.ResponseWriter, r *http.Request) (*Lobby, int, error) { +func validatePlayer(res http.ResponseWriter, req *http.Request) (*lobby, int, error) { fmt.Println("validatePlayer") - lobby_id := r.URL.Query().Get("lobby_id") - //fmt.Printf("lobby_id: %s\n", lobby_id) + lobbyID := req.URL.Query().Get("lobby_id") // only continue with connection if lobby exists - lobby, ok := lobby_list[lobby_id] + lobby, ok := lobbyList[lobbyID] lobby.mutex.Lock() defer lobby.mutex.Unlock() // If the key doesn't exist, return error if !ok { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 - Lobby not found")) - return nil, 0, errors.New("Lobby not found") + res.WriteHeader(http.StatusNotFound) + _, err := res.Write([]byte("404 - Lobby not found")) + if err != nil { + fmt.Printf("Failed to write lobby_not_found: %s", err) + + return nil, 0, errLobbyNotFound + } + + return nil, 0, errLobbyNotFound } - player_id_string := r.URL.Query().Get("player_id") - player_id, err := strconv.Atoi(player_id_string) + playerIDString := req.URL.Query().Get("player_id") + playerID, err := strconv.Atoi(playerIDString) if err != nil { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 - Player not found")) - return nil, 0, errors.New("Player not found") + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Player not found")) + if err != nil { + fmt.Printf("Failed to write player_not_found: %s", err) + + return nil, 0, errPlayerNotFound + } + + return nil, 0, errPlayerNotFound } - //fmt.Printf("player_id: %d\n", player_id) - //fmt.Printf("length of lobby.Clients: %d\n", len(lobby_list[lobby_id].Clients)) - //fmt.Println(lobby.Clients) + // check if player actually exists - if player_id < 0 || player_id >= len(lobby.Clients) { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 - Player not found")) - return nil, 0, errors.New("Player not found") + if playerID < 0 || playerID >= len(lobby.Clients) { + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Player not found")) + if err != nil { + fmt.Printf("Failed to write player_not_found: %s", err) + + return nil, 0, errPlayerNotFound + } + + return nil, 0, errPlayerNotFound } - return lobby, player_id, nil + + return lobby, playerID, nil } -func offerGet(w http.ResponseWriter, r *http.Request) { +func offerGet(res http.ResponseWriter, req *http.Request) { fmt.Println("offerGet") - w.Header().Set("Content-Type", "application/json") + res.Header().Set("Content-Type", "application/json") - lobby, player_id, err := validatePlayer(w, r) + lobby, playerID, err := validatePlayer(res, req) if err != nil { return } lobby.mutex.Lock() defer lobby.mutex.Unlock() - offer := lobby.Clients[player_id].Offer + offer := lobby.Clients[playerID].Offer if offer == nil { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 - Offer not found")) + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Offer not found")) + if err != nil { + fmt.Printf("Failed to write offer: %s", err) + + return + } + return } - jsonValue, _ := json.Marshal(offer) + jsonValue, err := json.Marshal(offer) + if err != nil { + fmt.Printf("Failed to marshal offer: %s", err) + + return + } - io.Writer.Write(w, jsonValue) + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write offer: %s", err) - /* - fmt.Println("offerGet") - fmt.Println(jsonValue) - */ + return + } } -func offerPost(w http.ResponseWriter, r *http.Request) { +func offerPost(res http.ResponseWriter, req *http.Request) { fmt.Println("offerPost") - lobby, player_id, err := validatePlayer(w, r) + lobby, playerID, err := validatePlayer(res, req) if err != nil { return } @@ -264,21 +340,22 @@ func offerPost(w http.ResponseWriter, r *http.Request) { // Try to decode the request body into the struct. If there is an error, // respond to the client with the error message and a 400 status code. - err = json.NewDecoder(r.Body).Decode(&sdp) + err = json.NewDecoder(req.Body).Decode(&sdp) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(res, err.Error(), http.StatusBadRequest) + return } - lobby.Clients[player_id].Offer = &sdp + lobby.Clients[playerID].Offer = &sdp fmt.Printf("Lobby: %+v\n", lobby.Clients) } -func answerGet(w http.ResponseWriter, r *http.Request) { +func answerGet(res http.ResponseWriter, req *http.Request) { fmt.Println("answerGet") - w.Header().Set("Content-Type", "application/json") + res.Header().Set("Content-Type", "application/json") - lobby, player_id, err := validatePlayer(w, r) + lobby, playerID, err := validatePlayer(res, req) if err != nil { return } @@ -286,23 +363,39 @@ func answerGet(w http.ResponseWriter, r *http.Request) { lobby.mutex.Lock() defer lobby.mutex.Unlock() - answer := lobby.Clients[player_id].Answer + answer := lobby.Clients[playerID].Answer if answer == nil { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("404 - Answer not found")) + res.WriteHeader(http.StatusNotFound) + _, err = res.Write([]byte("404 - Answer not found")) + if err != nil { + fmt.Printf("Failed to write answer: %s", err) + + return + } + return } - jsonValue, _ := json.Marshal(answer) + jsonValue, err := json.Marshal(answer) + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) - io.Writer.Write(w, jsonValue) + return + } + + _, err = io.Writer.Write(res, jsonValue) + if err != nil { + fmt.Printf("Failed to write answer: %s", err) + + return + } } -func answerPost(w http.ResponseWriter, r *http.Request) { +func answerPost(res http.ResponseWriter, req *http.Request) { fmt.Println("answerPost") - w.Header().Set("Content-Type", "application/json") + res.Header().Set("Content-Type", "application/json") - lobby, player_id, err := validatePlayer(w, r) + lobby, playerID, err := validatePlayer(res, req) if err != nil { return } @@ -314,17 +407,17 @@ func answerPost(w http.ResponseWriter, r *http.Request) { // Try to decode the request body into the struct. If there is an error, // respond to the client with the error message and a 400 status code. - err = json.NewDecoder(r.Body).Decode(&sdp) + err = json.NewDecoder(req.Body).Decode(&sdp) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(res, err.Error(), http.StatusBadRequest) + return } - lobby.Clients[player_id].Answer = &sdp + lobby.Clients[playerID].Answer = &sdp fmt.Printf("Lobby: %+v\n", lobby.Clients) } -func ice(w http.ResponseWriter, r *http.Request) { - // TODO: Implement the ice handler +func ice(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") }