From 781ff736bfda464504ad830de1a04475a528ecd3 Mon Sep 17 00:00:00 2001 From: Sean DuBois Date: Fri, 12 Sep 2025 15:24:58 -0400 Subject: [PATCH] Create examples/data-channels-detach-create Pion <-> Pion DataChannels example Resolves #2706 --- .../data-channels-detach-create/README.md | 30 +++ examples/data-channels-detach-create/main.go | 211 ++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 examples/data-channels-detach-create/README.md create mode 100644 examples/data-channels-detach-create/main.go diff --git a/examples/data-channels-detach-create/README.md b/examples/data-channels-detach-create/README.md new file mode 100644 index 00000000..cb846eda --- /dev/null +++ b/examples/data-channels-detach-create/README.md @@ -0,0 +1,30 @@ +# data-channels-detach-create +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. + +The example is meant to be used with data-channels-detach. This demonstrates two Go Pion processes communicating directly. + +## Run data-channels-detach-create and make an offer to data-channels-detach via stdin +``` +go run data-channels-detach-create/*.go | go run data-channels-detach/*.go +``` + +## post the answer from data-channels-detach back to data-channels-detach-create +You will see a base64 SDP printed to your console. You now need to communicate this back to `data-channels-detach-create` this can be done via a HTTP endpoint + +`curl localhost:8080/sdp -d "BASE_64_SDP"` + +## Output + +On sucess you will get output like the following + +``` +Peer Connection State has changed: connecting +(Long base64 SDP that you should POST) +Peer Connection State has changed: connected +New DataChannel 1374394845054 +Data channel ''-'1374394845054' open. +Message from DataChannel: kvmWkjYodyQcIlv +Sending aMDnwlTfDYnfoUy +Sending htqQtnbvygZKlmy +Message from DataChannel: CMjZiNtsmIBpCaN +``` diff --git a/examples/data-channels-detach-create/main.go b/examples/data-channels-detach-create/main.go new file mode 100644 index 00000000..7bb1eae1 --- /dev/null +++ b/examples/data-channels-detach-create/main.go @@ -0,0 +1,211 @@ +// 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 ( + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "time" + + "github.com/pion/randutil" + "github.com/pion/webrtc/v4" +) + +const messageSize = 15 + +func main() { + sdpChan := httpSDPServer(8080) + + // 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) + } + }() + + // Set the handler for Peer connection state + // This will notify you when the peer has connected/disconnected + 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) + } + }) + + dataChannel, err := peerConnection.CreateDataChannel("", nil) + if err != nil { + panic(err) + } + + 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) + }) + + // Create an offer to send to the browser + offer, err := peerConnection.CreateOffer(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 + if err = peerConnection.SetLocalDescription(offer); 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 offer in base64 so we can paste it in browser + fmt.Println(encode(peerConnection.LocalDescription())) + + // Wait for the answer to be submitted via HTTP + answer := webrtc.SessionDescription{} + decode(<-sdpChan, &answer) + + // Set the remote SessionDescription + err = peerConnection.SetRemoteDescription(answer) + if err != nil { + panic(err) + } + + // Block forever + select {} +} + +// ReadLoop shows how to read from the datachannel directly. +func ReadLoop(d io.Reader) { + for { + buffer := make([]byte, messageSize) + n, err := d.Read(buffer) + if err != nil { + fmt.Println("Datachannel closed; Exit the readloop:", err) + + return + } + + fmt.Printf("Message from DataChannel: %s\n", string(buffer[:n])) + } +} + +// WriteLoop shows how to write to the datachannel directly. +func WriteLoop(d io.Writer) { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + for range ticker.C { + message, err := randutil.GenerateCryptoRandomString( + messageSize, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + ) + if err != nil { + panic(err) + } + + fmt.Printf("Sending %s \n", message) + if _, err := d.Write([]byte(message)); err != nil { + panic(err) + } + } +} + +// httpSDPServer starts a HTTP Server that consumes SDPs. +func httpSDPServer(port int) chan string { + sdpChan := make(chan string) + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + fmt.Fprintf(w, "done") //nolint: errcheck + sdpChan <- string(body) + }) + + go func() { + // nolint: gosec + panic(http.ListenAndServe(":"+strconv.Itoa(port), nil)) + }() + + return sdpChan +} + +// 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) + } +}