diff --git a/examples/warp/index.html b/examples/warp/index.html new file mode 100644 index 00000000000..b103b287bf8 --- /dev/null +++ b/examples/warp/index.html @@ -0,0 +1,117 @@ + + + + + + WARP: Faster WebRTC with SNAP and SPED + + +

📡 WebRTC DataChannel Test

+
+ + +
+ + +

+
+  
+
+
diff --git a/examples/warp/main.go b/examples/warp/main.go
new file mode 100644
index 00000000000..240148162ac
--- /dev/null
+++ b/examples/warp/main.go
@@ -0,0 +1,160 @@
+// SPDX-FileCopyrightText: 2025 The Pion community 
+// SPDX-License-Identifier: MIT
+
+//go:build !js
+// +build !js
+
+// WARP (SNAP+SPED) testbed.
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/pion/webrtc/v4"
+)
+
+func main() {
+	var pc *webrtc.PeerConnection
+
+	setupOfferHandler(&pc)
+	setupCandidateHandler(&pc)
+	setupStaticHandler()
+
+	fmt.Println("google-chrome-unstable --force-fieldtrials=" +
+		"WebRTC-Sctp-Snap/Enabled/WebRTC-IceHandshakeDtls/Enabled/ " +
+		"--disable-features=WebRtcPqcForDtls http://localhost:8080")
+	fmt.Printf("Add `--enable-logging --v=1` and then " +
+		"`grep SCTP_PACKET chrome_debug.log | " +
+		"text2pcap -D -u 1001,2001 -t \"%%H:%%M:%%S.%%f\" - out.pcap` " +
+		"for inspecting the raw packets.\n")
+	fmt.Println("🚀 Signaling server started on http://localhost:8080")
+	//nolint:gosec
+	if err := http.ListenAndServe(":8080", nil); err != nil {
+		fmt.Printf("Failed to start server: %v\n", err)
+	}
+}
+
+func setupOfferHandler(pc **webrtc.PeerConnection) {
+	http.HandleFunc("/offer", func(responseWriter http.ResponseWriter, r *http.Request) {
+		var offer webrtc.SessionDescription
+		if err := json.NewDecoder(r.Body).Decode(&offer); err != nil {
+			http.Error(responseWriter, err.Error(), http.StatusBadRequest)
+
+			return
+		}
+
+		var err error
+		*pc, err = webrtc.NewPeerConnection(webrtc.Configuration{
+			BundlePolicy: webrtc.BundlePolicyMaxBundle,
+		})
+		if err != nil {
+			http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
+
+			return
+		}
+
+		setupICECandidateHandler(*pc)
+		setupDataChannelHandler(*pc)
+
+		if err := processOffer(*pc, offer, responseWriter); err != nil {
+			http.Error(responseWriter, err.Error(), http.StatusInternalServerError)
+
+			return
+		}
+	})
+}
+
+func setupICECandidateHandler(pc *webrtc.PeerConnection) {
+	pc.OnICECandidate(func(c *webrtc.ICECandidate) {
+		if c != nil {
+			fmt.Printf("🌐 New ICE candidate: %s\n", c.Address)
+		}
+	})
+}
+
+func setupDataChannelHandler(pc *webrtc.PeerConnection) {
+	pc.OnDataChannel(func(d *webrtc.DataChannel) {
+		d.OnOpen(func() {
+			fmt.Println("✅ DataChannel opened (Server)")
+			if sendErr := d.SendText("Hello from Go server 👋"); sendErr != nil {
+				fmt.Printf("Failed to send text: %v\n", sendErr)
+			}
+		})
+		d.OnMessage(func(msg webrtc.DataChannelMessage) {
+			fmt.Printf("📩 Received: %s\n", string(msg.Data))
+			if sendErr := d.SendText("ECHO " + string(msg.Data)); sendErr != nil {
+				fmt.Printf("Failed to send text: %v\n", sendErr)
+			}
+		})
+	})
+	if serverDc, err := pc.CreateDataChannel("server-opened-channel", nil); err == nil {
+		serverDc.OnOpen(func() {
+			if sendErr := serverDc.SendText("Server opened channel ready"); sendErr != nil {
+				fmt.Printf("Failed to send on server-opened channel: %v\n", sendErr)
+			}
+		})
+	}
+}
+
+func processOffer(
+	pc *webrtc.PeerConnection,
+	offer webrtc.SessionDescription,
+	responseWriter http.ResponseWriter,
+) error {
+	// Set remote description
+	if err := pc.SetRemoteDescription(offer); err != nil {
+		return err
+	}
+
+	// Create answer
+	answer, err := pc.CreateAnswer(nil)
+	if err != nil {
+		return err
+	}
+
+	// Set local description
+	if err := pc.SetLocalDescription(answer); err != nil {
+		return err
+	}
+
+	// Wait for ICE gathering to complete before sending answer
+	gatherComplete := webrtc.GatheringCompletePromise(pc)
+	<-gatherComplete
+
+	finalAnswer := pc.LocalDescription()
+	if finalAnswer == nil {
+		//nolint:err113
+		return fmt.Errorf("local description is nil after ICE gathering")
+	}
+
+	responseWriter.Header().Set("Content-Type", "application/json")
+	if err := json.NewEncoder(responseWriter).Encode(*finalAnswer); err != nil {
+		fmt.Printf("Failed to encode answer: %v\n", err)
+	}
+
+	return nil
+}
+
+func setupCandidateHandler(pc **webrtc.PeerConnection) {
+	http.HandleFunc("/candidate", func(responseWriter http.ResponseWriter, r *http.Request) {
+		var candidate webrtc.ICECandidateInit
+		if err := json.NewDecoder(r.Body).Decode(&candidate); err != nil {
+			http.Error(responseWriter, err.Error(), http.StatusBadRequest)
+
+			return
+		}
+		if *pc != nil {
+			if err := (*pc).AddICECandidate(candidate); err != nil {
+				fmt.Println("Failed to add candidate", err)
+			}
+		}
+	})
+}
+
+func setupStaticHandler() {
+	http.HandleFunc("/", func(responseWriter http.ResponseWriter, r *http.Request) {
+		http.ServeFile(responseWriter, r, "./index.html")
+	})
+}