diff --git a/go.mod b/go.mod index 7974515e..b498b671 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,16 @@ module github.com/pion/example-webrtc-applications/v3 -go 1.22 +go 1.23.0 -toolchain go1.23.6 +toolchain go1.24.2 require ( github.com/asticode/go-astiav v0.19.0 github.com/at-wat/ebml-go v0.17.1 - github.com/emiago/sipgo v0.33.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.8 github.com/notedit/janus-go v0.0.0-20210115013133-fdce1b146d0e github.com/pion/interceptor v0.1.40 github.com/pion/logging v0.2.4 @@ -25,12 +25,16 @@ require ( require ( 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.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.3.2 // indirect - github.com/icholy/digest v1.1.0 // indirect github.com/mattn/go-pointer v0.0.1 // 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 @@ -41,9 +45,12 @@ 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/rivo/uniseg v0.4.7 // indirect github.com/rs/xid v1.5.0 // 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/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index cf080270..eb18c6a9 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,14 @@ 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/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.33.0 h1:UxPKCoPREffSjrRE6oesG/RPz5/ZSp8tA8Jc6YvYUsk= -github.com/emiago/sipgo v0.33.0/go.mod h1:gbOLw/kZHZ3wS/5PIa9qVjpdil/IKLdigbZFIYFpHTs= 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/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= @@ -24,12 +26,12 @@ 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 v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= -github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= 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/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= @@ -60,29 +62,22 @@ 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.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= -github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= -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/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 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.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.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= 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.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= -golang.org/x/sys v0.6.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= 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= diff --git a/simple-ebiten-game/LICENSE.md b/simple-ebiten-game/LICENSE.md new file mode 100644 index 00000000..00ae6127 --- /dev/null +++ b/simple-ebiten-game/LICENSE.md @@ -0,0 +1,7 @@ +## 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/simple-ebiten-game/README.md b/simple-ebiten-game/README.md new file mode 100644 index 00000000..32145a0a --- /dev/null +++ b/simple-ebiten-game/README.md @@ -0,0 +1,13 @@ +# 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 trade SDPs. + +Do ``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)) + +To play: Just move around with the arrow keys once you have connected! + +Right now this only supports two clients connected to each other \ No newline at end of file diff --git a/simple-ebiten-game/gopher.png b/simple-ebiten-game/gopher.png new file mode 100644 index 00000000..b11c1703 Binary files /dev/null and b/simple-ebiten-game/gopher.png differ diff --git a/simple-ebiten-game/gopher.png.license b/simple-ebiten-game/gopher.png.license new file mode 100644 index 00000000..54b7f842 --- /dev/null +++ b/simple-ebiten-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 diff --git a/simple-ebiten-game/main.go b/simple-ebiten-game/main.go new file mode 100644 index 00000000..3ebbce6e --- /dev/null +++ b/simple-ebiten-game/main.go @@ -0,0 +1,375 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +// data-channels-detach is an example that shows how you can detach a data channel. +// This allows direct access the underlying [pion/datachannel](https://github.com/pion/datachannel). +// This allows you to interact with the data channel using a more idiomatic API based on +// the `io.ReadWriteCloser` interface. +package main + +import ( + "bufio" + "encoding/base64" + "encoding/gob" + "encoding/json" + "errors" + "fmt" + "image" + "io" + "log" + "os" + "strings" + "time" + + "github.com/ebitengine/debugui" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/pion/webrtc/v4" +) + +type NetworkMessage struct { + X float64 + Y float64 +} + +var img *ebiten.Image +var player_x float64 = 0 +var player_y float64 = 0 + +var remote_player_x float64 = 0 +var remote_player_y float64 = 0 + +const PlayerSpeed = 2 + +var isClient bool = false +var isHost bool = false + +func init() { + var err error + img, _, err = ebitenutil.NewImageFromFile("gopher.png") + if err != nil { + log.Fatal(err) + } +} + +type Game struct { + debugui debugui.DebugUI + peerConnection *webrtc.PeerConnection +} + +func (g *Game) Update() error { + // Update player position based on input + if ebiten.IsKeyPressed(ebiten.KeyArrowUp) { + player_y -= PlayerSpeed + } + if ebiten.IsKeyPressed(ebiten.KeyArrowDown) { + player_y += PlayerSpeed + } + if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { + player_x -= PlayerSpeed + } + if ebiten.IsKeyPressed(ebiten.KeyArrowRight) { + player_x += PlayerSpeed + } + + // ui stuff + if _, err := g.debugui.Update(func(ctx *debugui.Context) error { + ctx.Window("Test", image.Rect(60, 60, 160, 180), func(layout debugui.ContainerLayout) { + ctx.Button("Host Button").On(func() { + if !isHost { + g.runHost() + isHost = true + } + }) + ctx.Button("Client Button").On(func() { + if !isClient { + g.runClient() + isClient = true + } + }) + }) + return nil + }); err != nil { + return err + } + + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + // local player + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(player_x, player_y) + screen.DrawImage(img, op) + // remote player + op = &ebiten.DrawImageOptions{} + op.GeoM.Translate(remote_player_x, remote_player_y) + screen.DrawImage(img, op) + + // render debug UI + g.debugui.Draw(screen) +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { + return outsideWidth, outsideHeight +} + +func (g *Game) networkSetup() { + // 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 + peerConnection, err := api.NewPeerConnection(config) + if err != nil { + panic(err) + } + /* + defer func() { + if cErr := peerConnection.Close(); cErr != nil { + fmt.Printf("cannot close peerConnection: %v\n", cErr) + } + }() + */ + + g.peerConnection = peerConnection + + // Set the handler for Peer connection state + // This will notify you when the peer has connected/disconnected + g.peerConnection.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { + fmt.Printf("Peer Connection State has changed: %s\n", state.String()) + + if state == 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. + fmt.Println("Peer Connection has gone to failed exiting") + os.Exit(0) + } + + if state == webrtc.PeerConnectionStateClosed { + // PeerConnection was explicitly closed. This usually happens from a DTLS CloseNotify + fmt.Println("Peer Connection has gone to closed exiting") + os.Exit(0) + } + }) +} + +func (g *Game) runClient() { + g.networkSetup() + // Create a data channel with the default label and options + dataChannel, err := g.peerConnection.CreateDataChannel("data", nil) + if err != nil { + panic(err) + } + fmt.Printf("Created DataChannel %s %d\n", dataChannel.Label(), dataChannel.ID()) + + // Register channel opening handling + dataChannel.OnOpen(func() { + fmt.Printf("Data channel '%s'-'%d' open.\n", dataChannel.Label(), dataChannel.ID()) + + // Detach the data channel + raw, dErr := dataChannel.Detach() + if dErr != nil { + panic(dErr) + } + + // Handle reading from the data channel + go ReadLoop(raw) + + // Handle writing to the data channel + go WriteLoop(raw) + }) + + offer, err := g.peerConnection.CreateOffer(nil) + if err != nil { + panic(err) + } + + err = g.peerConnection.SetLocalDescription(offer) + if err != nil { + panic(err) + } + + // Output the answer in base64 so we can paste it in browser + fmt.Println("Printing SDP Offer, give this to the client:") + fmt.Println(encode(&offer)) + + // Wait for the answer to be pasted + fmt.Println("Waiting for answer from client:") + answer := webrtc.SessionDescription{} + decode(readUntilNewline(), &answer) + + // Set the remote SessionDescription + err = g.peerConnection.SetRemoteDescription(answer) + if err != nil { + panic(err) + } + + fmt.Println("Remote description set, client should now be able to connect") +} + +func (g *Game) runHost() { + g.networkSetup() + // callback for when we receive a new data channel + // Register data channel creation handling + g.peerConnection.OnDataChannel(func(dataChannel *webrtc.DataChannel) { + fmt.Printf("New DataChannel %s %d\n", dataChannel.Label(), dataChannel.ID()) + + // Register channel opening handling + dataChannel.OnOpen(func() { + fmt.Printf("Data channel '%s'-'%d' open.\n", dataChannel.Label(), dataChannel.ID()) + + // Detach the data channel + raw, dErr := dataChannel.Detach() + if dErr != nil { + panic(dErr) + } + + // Handle reading from the data channel + go ReadLoop(raw) + + // Handle writing to the data channel + go WriteLoop(raw) + }) + }) + fmt.Println("Waiting for SDP Offer from host:") + // Wait for the offer to be pasted + offer := webrtc.SessionDescription{} + decode(readUntilNewline(), &offer) + + // Set the remote SessionDescription + err := g.peerConnection.SetRemoteDescription(offer) + if err != nil { + panic(err) + } + + // Create answer + answer, err := g.peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } + + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(g.peerConnection) + + // Sets the LocalDescription, and starts our UDP listeners + err = g.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 + + // Output the answer in base64 so we can paste it in browser + fmt.Println("Printing SDP Answer, give this to the host:") + fmt.Println(encode(g.peerConnection.CurrentLocalDescription())) +} + +func main() { + ebiten.SetWindowSize(640, 480) + ebiten.SetWindowTitle("Render an image") + if err := ebiten.RunGame(&Game{}); err != nil { + log.Fatal(err) + } +} + +// ReadLoop shows how to read from the datachannel directly. +func ReadLoop(d io.Reader) { + dec := gob.NewDecoder(d) + for { + var message NetworkMessage + if err := dec.Decode(&message); err != nil { + fmt.Println("Datachannel closed; Exit the readloop:", err) + + return + } + + //fmt.Printf("Message from DataChannel: %#v\n", message) + remote_player_x = message.X + remote_player_y = message.Y + } +} + +// WriteLoop shows how to write to the datachannel directly. +func WriteLoop(d io.Writer) { + enc := gob.NewEncoder(d) + ticker := time.NewTicker(16 * time.Millisecond) // roughly 60 FPS + defer ticker.Stop() + for range ticker.C { + message := NetworkMessage{ + X: player_x, + Y: player_y, + } + //fmt.Printf("Sending %#v \n", message) + if err := enc.Encode(message); err != nil { + fmt.Println("Datachannel closed; Exit the writeloop:", err) + panic(err) + } + } +} + +// Read from stdin until we get a newline. +func readUntilNewline() (in string) { + var err error + + r := bufio.NewReader(os.Stdin) + for { + in, err = r.ReadString('\n') + if err != nil && !errors.Is(err, io.EOF) { + panic(err) + } + + if in = strings.TrimSpace(in); len(in) > 0 { + break + } + } + + fmt.Println("") + + return +} + +// JSON encode + base64 a SessionDescription. +func encode(obj *webrtc.SessionDescription) string { + b, err := json.Marshal(obj) + if err != nil { + panic(err) + } + + return base64.StdEncoding.EncodeToString(b) +} + +// Decode a base64 and unmarshal JSON into a SessionDescription. +func decode(in string, obj *webrtc.SessionDescription) { + b, err := base64.StdEncoding.DecodeString(in) + if err != nil { + panic(err) + } + + if err = json.Unmarshal(b, obj); err != nil { + panic(err) + } +}