The missing debugger for WebRTC + WebSocket developers.
Paste two SDP strings. Instantly get a visual diff, browser source detection, and plain-English diagnosis of every failure — with a shareable link your teammate can open in one click.
Built for developers who have ever stared at
chrome://webrtc-internalsat 2am wondering why their video call is silently failing.
WebRTC debugging is uniquely painful. When a call fails, you're left with:
- Raw SDP text with no visual structure
chrome://webrtc-internalsshowing stats with no explanation- No connection between the signaling decisions you made and the media failure you got
SignalFlow fixes the first problem completely. You paste your Offer and Answer SDPs, and within milliseconds you see:
- What changed between them — codec removals, ICE credential mismatches, fingerprint differences — in a color-coded visual diff
- Why it might fail — 25 diagnostic rules covering ICE, DTLS, codecs, simulcast, and BUNDLE, each with a plain-English explanation and a concrete fix
- Who generated it — automatic detection of Chrome, Firefox, Safari, LiveKit, mediasoup, Pion, Daily.co, and more
- A shareable link — your entire comparison encoded in the URL, no backend, no login
Try a pre-loaded failure scenario on the homepage — no SDP required to see the tool in action.
[Homepage with example gallery]
[SDP comparison with diff viewer and issues panel]
[Mobile layout]
Compares two SDP strings semantically — not line by line. Sections are matched by mid value, so a reordered SDP doesn't produce false positives.
Each media section (audio, video, data channel) is collapsible and shows:
| Field | Before | After | Status |
|---|---|---|---|
| Codecs | VP8, AV1 | VP8 | 🔴 removed |
| ICE ufrag | abc123 | xyz789 | 🟡 changed |
| Direction | sendrecv | sendrecv | ✅ same |
Color coding: green = added, red = removed, yellow = changed.
Paste any SDP and SignalFlow identifies where it came from using signature detection:
| Source | Detection signals |
|---|---|
| Chrome | extmap-allow-mixed present, sha-256 fingerprint, AV1 codec |
| Firefox | Origin address 0.0.0.0, no extmap-allow-mixed, specific header extension URIs |
| Safari | sha-1 fingerprint, no AV1 support |
| LiveKit | origin.username = livekit or livekit in raw |
| mediasoup | origin.username = mediasoup |
| Pion | origin.username = pion or pion- in raw |
| Daily.co | origin.username = daily or daily.co in raw |
| Janus | origin.username = janus |
Every rule returns a severity level, a plain-English explanation of what went wrong, and a concrete fix — written like a Stack Overflow answer, not a spec reference.
| ID | Severity | What it catches |
|---|---|---|
ice-missing-credentials |
🔴 Error | No a=ice-ufrag or a=ice-pwd in a media section |
ice-no-turn-candidates |
🟡 Warning | No relay candidates — will fail for ~15% of users behind symmetric NAT |
ice-same-credentials |
🔴 Error | Offer and answer have identical ICE ufrag — copy-paste error |
ice-only-host-candidates |
🟡 Warning | STUN gathering incomplete, only local addresses present |
ice-no-candidates-at-all |
🔴 Error | Zero ICE candidates across all media sections |
| ID | Severity | What it catches |
|---|---|---|
dtls-missing-fingerprint |
🔴 Error | No a=fingerprint — DTLS handshake will fail immediately |
dtls-sha1-fingerprint |
🟡 Warning | SHA-1 fingerprint detected (weak, legacy Safari) |
dtls-role-conflict |
🔴 Error | Both sides claim DTLS active — handshake deadlock |
| ID | Severity | What it catches |
|---|---|---|
codec-no-common-video |
🔴 Error | No shared video codec between offer and answer |
codec-av1-not-negotiated |
ℹ️ Info | AV1 offered but not accepted — falling back to VP9/VP8 |
codec-no-opus |
🟡 Warning | Opus missing from audio section |
codec-missing-rtx |
🟡 Warning | RTX retransmission stripped — poor quality on lossy networks |
codec-opus-stereo-mismatch |
ℹ️ Info | Stereo Opus parameter disagreement between peers |
| ID | Severity | What it catches |
|---|---|---|
simulcast-not-accepted |
🟡 Warning | Simulcast offered but answer didn't accept it |
simulcast-rid-mismatch |
🔴 Error | RID values don't match — SFU will drop video layers |
simulcast-missing-rtx |
🟡 Warning | Simulcast layers without RTX support |
| ID | Severity | What it catches |
|---|---|---|
bundle-missing |
🟡 Warning | No BUNDLE group — each media section opens a separate connection |
bundle-mid-mismatch |
🔴 Error | BUNDLE group references MIDs that don't exist in media sections |
| ID | Severity | What it catches |
|---|---|---|
rtcp-mux-missing |
🟡 Warning | No a=rtcp-mux — using double the ports |
data-channel-sctp-missing |
🔴 Error | Data channel section has no SCTP config |
no-candidates-at-all |
🔴 Error | Absolutely zero ICE candidates anywhere |
msid-missing-video |
🟡 Warning | Video has no msid — track won't attach to MediaStream |
extmap-allow-mixed-mismatch |
ℹ️ Info | Header extension policy differs between peers |
session-fingerprint-conflict |
🔴 Error | Session-level and media-level fingerprints conflict |
ice-lite-mismatch |
🔴 Error | ICE lite declared on one side only |
Both SDP strings are compressed using pako (deflate) and encoded into the URL fragment. No server. No database. No login.
signalflow.dev/compare#H4sIAAAAAAAAE6tWKkktLlGyUlIqS...
Open the link → exact comparison loads instantly. Works offline.
Capture SDP from any WebRTC app without touching the source code:
// Paste in browser DevTools on any WebRTC app
const _orig = RTCPeerConnection.prototype.setLocalDescription;
RTCPeerConnection.prototype.setLocalDescription = function(desc) {
if (desc?.sdp) {
console.log('%c[SignalFlow] ' + desc.type.toUpperCase(), 'color: #34d399; font-weight: bold');
console.log(desc.sdp);
}
return _orig.apply(this, arguments);
};Works on Google Meet, Discord, your own app — anything that uses WebRTC in the browser.
signalflow/
├── apps/
│ └── dashboard/ ← Next.js 15 App Router
│ ├── app/
│ │ ├── page.tsx ← Homepage + example gallery
│ │ ├── compare/
│ │ │ └── page.tsx ← Main SDP diff page
│ │ └── docs/
│ │ └── page.tsx ← SEO docs
│ ├── components/
│ │ ├── SdpTextarea.tsx
│ │ ├── SdpDiffViewer.tsx
│ │ ├── IssuesPanel.tsx
│ │ ├── ShareButton.tsx
│ │ └── BrowserBadge.tsx
│ └── lib/
│ ├── share.ts ← pako URL compression
│ └── example-sdps.ts ← Pre-loaded failure examples
│
└── packages/
└── shared/ ← @signalflow/shared (pure TypeScript)
└── src/
├── types.ts ← All interfaces
├── parser.ts ← sdp-transform wrapper
├── detector.ts ← Browser/SDK detection
├── diff/
│ └── sdp-diff.ts ← Semantic diff engine
└── diagnostics/
├── index.ts ← Rule runner
├── ice.ts
├── dtls.ts
├── codecs.ts
├── simulcast.ts
└── bundle.ts
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 15 App Router | App Router + static export for Cloudflare |
| Styling | Tailwind CSS + shadcn/ui | Fast, consistent dark UI |
| SDP parsing | sdp-transform |
2M+ weekly downloads, used by LiveKit, JsSIP, Matrix |
| URL compression | pako (deflate) |
Zero backend, SDPs compress 70-80% |
| State | React useState + URL fragment |
No server state needed in V1 |
| Monorepo | pnpm workspaces | Clean shared package without npm publish |
| Deployment | Cloudflare Pages | Free, global CDN, static export |
| CI | GitHub Actions | Auto-deploy on push to main |
- Node.js 20+
- pnpm 9+
# Install pnpm if you don't have it
npm install -g pnpm# Clone the repo
git clone https://github.com/yourusername/signalflow.git
cd signalflow
# Install all dependencies across the monorepo
pnpm install
# Build the shared package first
pnpm --filter @signalflow/shared build
# Start the development server
pnpm devOpen http://localhost:3000.
# Build everything
pnpm build
# Preview the static export locally
npx serve apps/dashboard/out -p 3001The core SDP logic is in a separate package and can be used independently:
import { parseSDP } from '@signalflow/shared';
const sdp = parseSDP(`v=0
o=- 1234 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
m=audio 9 UDP/TLS/RTP/SAVPF 111
...`);
console.log(sdp.source); // 'Chrome'
console.log(sdp.media[0].codecs); // [{ name: 'opus', payloadType: 111, ... }]
console.log(sdp.media[0].iceUfrag); // 'abc123'import { parseSDP, diffSDPs } from '@signalflow/shared';
const offer = parseSDP(offerString);
const answer = parseSDP(answerString);
const result = diffSDPs(offer, answer);
console.log(result.summary);
// { errors: 1, warnings: 2, changes: 3, additions: 0, removals: 1 }
result.items.forEach(item => {
console.log(`[${item.type}] ${item.label}: ${item.valueBefore} → ${item.valueAfter}`);
});import { parseSDP, runDiagnostics } from '@signalflow/shared';
const offer = parseSDP(offerString);
const answer = parseSDP(answerString);
const issues = runDiagnostics(offer, answer);
issues.forEach(issue => {
console.log(`[${issue.severity}] ${issue.title}`);
console.log(` Why: ${issue.explanation}`);
console.log(` Fix: ${issue.fix}`);
});import { parseSDP } from '@signalflow/shared';
const sdp = parseSDP(rawSdpString);
console.log(sdp.source); // 'Chrome' | 'Firefox' | 'Safari' | 'LiveKit' | ...Paste this into DevTools before starting a call:
const _orig = RTCPeerConnection.prototype.setLocalDescription;
RTCPeerConnection.prototype.setLocalDescription = function(desc) {
if (desc?.sdp) {
console.log('%c[SignalFlow] ' + desc.type.toUpperCase(), 'color: #34d399; font-weight: bold');
console.log(desc.sdp);
}
return _orig.apply(this, arguments);
};- Open
chrome://webrtc-internalsin a new tab - Start your WebRTC call in another tab
- Find your PeerConnection entry
- Expand "setLocalDescription" or "setRemoteDescription"
- Copy the
sdpfield value
If your signaling server logs SDP exchanges (it should), grep for "type":"offer" and "type":"answer" entries.
# Build shared package
cd packages/shared && pnpm build
# Run the test suite
pnpm test
# Individual logic tests (Node.js, no browser needed)
node tests/test-parser.mjs
node tests/test-detector.mjs
node tests/test-diff.mjs
node tests/test-diagnostics.mjs
node tests/test-share.mjsExpected output for a passing suite:
✅ Parsed OK
✅ Chrome detected
✅ LiveKit detected
✅ AV1 removal detected
✅ Fingerprint change detected
✅ ice-same-credentials [error]
✅ ice-no-turn-candidates [warning]
✅ ice-only-host-candidates [warning]
✅ codec-av1-not-negotiated [info]
✅ codec-missing-rtx [warning]
✅ sdp1 round-trips correctly
✅ sdp2 round-trips correctly
✅ Empty hash returns null
✅ Invalid hash returns null
# One-time setup
npm install -g wrangler
wrangler login
# Deploy
pnpm deployThe deploy script builds the Next.js static export and pushes to Cloudflare Pages automatically.
Add these secrets to your repository:
| Secret | Value |
|---|---|
CF_API_TOKEN |
Cloudflare API token with Pages:Edit permission |
Every push to main triggers a build and deploy automatically.
Since this is a pure static export (output: 'export' in next.config), it deploys to any static host:
pnpm --filter dashboard build
# Output is in apps/dashboard/out/
# Upload that folder to Vercel, Netlify, S3, GitHub Pages, etc.| Tool | What it does | What it can't do |
|---|---|---|
chrome://webrtc-internals |
Shows raw WebRTC stats | No diff, no explanation, not shareable |
| Postman / Insomnia | Test WebSocket messages | No WebRTC awareness at all |
| Wireshark | Deep packet inspection | No SDP semantics, no diagnosis |
| rtcStats | Collects getStats() data |
No signaling layer, no diff, rule-based only |
diff / VSCode diff |
Text comparison | Line-based noise, no semantic understanding |
SignalFlow is the only tool that combines semantic SDP diff + browser detection + diagnostic rules + shareable links in one place, built specifically for the WebRTC debugging workflow.
npm install @signalflow/sdk— one-line drop-in that wrapsRTCPeerConnectionand captures everything automatically- Live session view showing ICE gathering progress and DTLS handshake state in real time
- Correlated WebSocket signaling ↔ WebRTC peer state timeline (the feature no other tool has)
- Expand diagnostic rules to 50+ (H.265, SFrame E2EE, scalability modes)
- AI diagnosis powered by Claude API — natural language root cause analysis from session data
- Chaos injection: slow signaling, radio silence, packet loss simulation without custom proxies
- Multi-session analytics: cluster failures by root cause across hundreds of sessions
Contributions are welcome, especially:
- New diagnostic rules — if you've hit a specific WebRTC failure that isn't caught, open an issue with the SDP that triggered it
- New browser/SDK signatures — if your SFU or browser isn't detected correctly
- Better rule explanations — if an explanation isn't clear enough for a non-expert
- Identify which category it belongs to:
ice,dtls,codecs,simulcast, orbundle - Add your rule function to the relevant file in
packages/shared/src/diagnostics/ - The function signature is:
(sdp1: ParsedSDP, sdp2: ParsedSDP) => DiagnosticIssue | null - Return
nullif the condition is NOT met — no false positives - Write the
explanationas 2-3 sentences a junior developer can understand - Write the
fixas a concrete action, not a vague suggestion - Add a test SDP that triggers your rule to the test suite
// Example: new rule in packages/shared/src/diagnostics/ice.ts
export const myNewRule: DiagnosticRule = (sdp1, sdp2) => {
// return null if condition not met
if (conditionNotMet) return null;
return {
id: 'ice-my-new-rule',
severity: 'warning',
category: 'ICE',
title: 'Short title that appears in the issues list',
explanation: 'Plain English explanation of what went wrong and why it matters. 2-3 sentences.',
fix: 'Concrete action the developer should take. No jargon.',
};
};git clone https://github.com/yourusername/signalflow.git
cd signalflow
pnpm install
pnpm --filter @signalflow/shared build
pnpm devThe dev server hot-reloads. Changes to packages/shared require a rebuild:
# In a separate terminal
cd packages/shared && pnpm build --watchMIT — do whatever you want with it.
If SignalFlow saves you time, consider:
- Starring the repo
- Sharing it in a WebRTC Discord when someone asks "why is my call failing"
- Opening a PR with a new diagnostic rule from a bug you actually hit
- sdp-transform — the SDP parsing library this is built on. Used by LiveKit, JsSIP, and the Matrix SDK.
- WebRTC spec and RFC 4566 — the primary sources for every diagnostic rule
- Every developer who has ever posted "why is my WebRTC call failing" in a Discord server — this tool is for you
Built by COSC · cosc25.in