Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions examples/warp/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html>
<!--
SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
SPDX-License-Identifier: MIT
-->
<head>
<meta charset="utf-8">
<title>WARP: Faster WebRTC with SNAP and SPED</title>
</head>
<body>
<h2>📡 WebRTC DataChannel Test</h2>
<div>
<label><input type="checkbox" id="dtlsRole" />Act as DTLS client</label>
<button id="startBtn" onclick="start()">Start</button>
</div>
<input id="msg" placeholder="Message">
<button id="sendBtn" disabled onclick="sendMsg()">Send</button>
<pre id="log"></pre>

<script>
const pc = new RTCPeerConnection();
const channel = pc.createDataChannel("chat");

pc.onconnectionstatechange = async () => {
log(`🔄 Connection state: ${pc.connectionState}`);
if (pc.connectionState === 'connected') {
const stats = await pc.getStats();
const transport = [...stats.values()].find(o => o.type === 'transport');
if (transport) {
log(`DTLS role: ${transport.dtlsRole}`);
const pair = stats.get(transport.selectedCandidatePairId);
if (pair) {
log(`RTT: ${pair.totalRoundTripTime / pair.responsesReceived}`);
}
}
}
}
pc.oniceconnectionstatechange = () => log(`🧊 ICE state: ${pc.iceConnectionState}`);
pc.onsignalingstatechange = () => log(`📞 Signaling state: ${pc.signalingState}`);

channel.onopen = () => {
log("✅ DataChannel opened");
document.getElementById("sendBtn").disabled = false;
}
channel.onmessage = e => log(`📩 Server: ${e.data}`);

pc.onicecandidate = event => {
if(event.candidate){
fetch("/candidate", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(event.candidate),
});
}
};
pc.ondatachannel = event => {
log("Server opened a channel", event.channel.name)
event.channel.onmessage = (ev) => {
log(`Server sent: ${ev.data}`)
}
};

async function start(){
document.getElementById("startBtn").disabled = true;
document.getElementById("dtlsRole").disabled = true;
try {
await pc.setLocalDescription();
const offer = pc.localDescription;
if (offer.sdp.indexOf("\na=sctp-init:") !== -1) {
log("✅ sctp-init found in offer, SNAP is supported");
}

const sdp = document.getElementById("dtlsRole").checked
? offer.sdp.replace("actpass", "active")
: offer.sdp;
// TODO: parameters to disable SPED/SNAP?
const res = await fetch("/offer", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({type: "offer", sdp}),
})

if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
}

const answer = await res.json();
if (answer.sdp.indexOf("\na=sctp-init:") !== -1) {
log("✅ sctp-init found in answer, SNAP is supported");
}
await pc.setRemoteDescription(answer);

} catch (err) {
log(`❌ Connection failed: ${err.message}`);
console.error("Connection error:", err);
}
}

function sendMsg(){
const msg = document.getElementById("msg").value;

if (msg.trim()) {
channel.send(msg);
log(`You: ${msg}`);
document.getElementById("msg").value = "";
}
}

function log(msg){
document.getElementById("log").textContent+=msg+"\n";
}


</script>
</body>
</html>
160 changes: 160 additions & 0 deletions examples/warp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
// 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")
})
}
Loading