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 00000000..b11c1703 Binary files /dev/null and b/ebiten-game/game/gopher.png differ 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 80503ffb..75a6df77 100644 --- a/go.mod +++ b/go.mod @@ -1,53 +1,64 @@ 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 github.com/pion/rtcp v1.2.15 - github.com/pion/rtp v1.8.11 - github.com/pion/sdp/v3 v3.0.10 - github.com/pion/webrtc/v4 v4.0.9 + github.com/pion/rtp v1.8.13 + github.com/pion/sdp/v3 v3.0.11 + github.com/pion/webrtc/v4 v4.0.14 + github.com/rs/cors v1.11.1 gocv.io/x/gocv v0.40.0 - golang.org/x/image v0.23.0 - golang.org/x/net v0.34.0 + golang.org/x/image v0.26.0 + golang.org/x/net v0.39.0 ) 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.4 // indirect - github.com/pion/ice/v4 v4.0.6 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.35 // indirect + github.com/pion/sctp v1.8.37 // indirect github.com/pion/srtp/v3 v3.0.4 // indirect 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.32.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect + golang.org/x/sync v0.13.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index f6b796b3..bd19f480 100644 --- a/go.sum +++ b/go.sum @@ -1,54 +1,68 @@ -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.4 h1:44CZekewMzfrn9pmGrj5BNnTMDCFwr+6sLH+cCuLM7U= -github.com/pion/dtls/v3 v3.0.4/go.mod h1:R373CsjxWqNPf6MEkfdy3aSe9niZvL/JaKlGeFphtMg= -github.com/pion/ice/v4 v4.0.6 h1:jmM9HwI9lfetQV/39uD0nY4y++XZNPhvzIPCb8EwxUM= -github.com/pion/ice/v4 v4.0.6/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= @@ -59,12 +73,12 @@ github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.11 h1:17xjnY5WO5hgO6SD3/NTIUPvSFw/PbLsIJyz1r1yNIk= -github.com/pion/rtp v1.8.11/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.35 h1:qwtKvNK1Wc5tHMIYgTDJhfZk7vATGVHhXbUDfHbYwzA= -github.com/pion/sctp v1.8.35/go.mod h1:EcXP8zCYVTRy3W9xtOF7wJm1L1aXfKRQzaM33SjQlzg= -github.com/pion/sdp/v3 v3.0.10 h1:6MChLE/1xYB+CjumMw+gZ9ufp2DPApuVSnDT8t5MIgA= -github.com/pion/sdp/v3 v3.0.10/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/rtp v1.8.13 h1:8uSUPpjSL4OlwZI8Ygqu7+h2p9NPFB+yAZ461Xn5sNg= +github.com/pion/rtp v1.8.13/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= +github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= +github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI= +github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= github.com/pion/srtp/v3 v3.0.4 h1:2Z6vDVxzrX3UHEgrUyIGM4rRouoC7v+NiF1IHtp9B5M= github.com/pion/srtp/v3 v3.0.4/go.mod h1:1Jx3FwDoxpRaTh1oRV8A/6G1BnFL+QI82eK4ms8EEJQ= github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= @@ -73,50 +87,107 @@ github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1 github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= 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.0.9 h1:PyOYMRKJgfy0dzPcYtFD/4oW9zaw3Ze3oZzzbj2LV9E= -github.com/pion/webrtc/v4 v4.0.9/go.mod h1:ViHLVaNpiuvaH8pdiuQxuA9awuE6KVzAXx3vVWilOck= -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/pion/webrtc/v4 v4.0.14 h1:nyds/sFRR+HvmWoBa6wrL46sSfpArE0qR883MBW96lg= +github.com/pion/webrtc/v4 v4.0.14/go.mod h1:R3+qTnQTS03UzwDarYecgioNf7DYgTsldxnCXB821Kk= +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= +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= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -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/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= +golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY= +golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= +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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 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.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.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/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=