Skip to content

Repository files navigation

SignalFlow

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-internals at 2am wondering why their video call is silently failing.


What is this?

WebRTC debugging is uniquely painful. When a call fails, you're left with:

  • Raw SDP text with no visual structure
  • chrome://webrtc-internals showing 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

Live demo

👉 signalflow.cosc25.in

Try a pre-loaded failure scenario on the homepage — no SDP required to see the tool in action.


Screenshots

[Homepage with example gallery]
[SDP comparison with diff viewer and issues panel]
[Mobile layout]

Features

SDP Visual Diff

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:

FieldBeforeAfterStatus
CodecsVP8, AV1VP8🔴 removed
ICE ufragabc123xyz789🟡 changed
Directionsendrecvsendrecv✅ same

Color coding: green = added, red = removed, yellow = changed.

Browser / SDK Auto-Detection

Paste any SDP and SignalFlow identifies where it came from using signature detection:

SourceDetection signals
Chromeextmap-allow-mixed present, sha-256 fingerprint, AV1 codec
FirefoxOrigin address 0.0.0.0, no extmap-allow-mixed, specific header extension URIs
Safarisha-1 fingerprint, no AV1 support
LiveKitorigin.username = livekit or livekit in raw
mediasouporigin.username = mediasoup
Pionorigin.username = pion or pion- in raw
Daily.coorigin.username = daily or daily.co in raw
Janusorigin.username = janus

25 Diagnostic Rules

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.

ICE Rules

IDSeverityWhat it catches
ice-missing-credentials🔴 ErrorNo a=ice-ufrag or a=ice-pwd in a media section
ice-no-turn-candidates🟡 WarningNo relay candidates — will fail for ~15% of users behind symmetric NAT
ice-same-credentials🔴 ErrorOffer and answer have identical ICE ufrag — copy-paste error
ice-only-host-candidates🟡 WarningSTUN gathering incomplete, only local addresses present
ice-no-candidates-at-all🔴 ErrorZero ICE candidates across all media sections

DTLS Rules

IDSeverityWhat it catches
dtls-missing-fingerprint🔴 ErrorNo a=fingerprint — DTLS handshake will fail immediately
dtls-sha1-fingerprint🟡 WarningSHA-1 fingerprint detected (weak, legacy Safari)
dtls-role-conflict🔴 ErrorBoth sides claim DTLS active — handshake deadlock

Codec Rules

IDSeverityWhat it catches
codec-no-common-video🔴 ErrorNo shared video codec between offer and answer
codec-av1-not-negotiatedℹ️ InfoAV1 offered but not accepted — falling back to VP9/VP8
codec-no-opus🟡 WarningOpus missing from audio section
codec-missing-rtx🟡 WarningRTX retransmission stripped — poor quality on lossy networks
codec-opus-stereo-mismatchℹ️ InfoStereo Opus parameter disagreement between peers

Simulcast Rules

IDSeverityWhat it catches
simulcast-not-accepted🟡 WarningSimulcast offered but answer didn't accept it
simulcast-rid-mismatch🔴 ErrorRID values don't match — SFU will drop video layers
simulcast-missing-rtx🟡 WarningSimulcast layers without RTX support

BUNDLE Rules

IDSeverityWhat it catches
bundle-missing🟡 WarningNo BUNDLE group — each media section opens a separate connection
bundle-mid-mismatch🔴 ErrorBUNDLE group references MIDs that don't exist in media sections

General Rules

IDSeverityWhat it catches
rtcp-mux-missing🟡 WarningNo a=rtcp-mux — using double the ports
data-channel-sctp-missing🔴 ErrorData channel section has no SCTP config
no-candidates-at-all🔴 ErrorAbsolutely zero ICE candidates anywhere
msid-missing-video🟡 WarningVideo has no msid — track won't attach to MediaStream
extmap-allow-mixed-mismatchℹ️ InfoHeader extension policy differs between peers
session-fingerprint-conflict🔴 ErrorSession-level and media-level fingerprints conflict
ice-lite-mismatch🔴 ErrorICE lite declared on one side only

Zero-Backend Shareable Links

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.

Console Snippet

Capture SDP from any WebRTC app without touching the source code:

// Paste in browser DevTools on any WebRTC appconst_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.


Project structure

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

Tech stack

LayerChoiceWhy
FrameworkNext.js 15 App RouterApp Router + static export for Cloudflare
StylingTailwind CSS + shadcn/uiFast, consistent dark UI
SDP parsingsdp-transform2M+ weekly downloads, used by LiveKit, JsSIP, Matrix
URL compressionpako (deflate)Zero backend, SDPs compress 70-80%
StateReact useState + URL fragmentNo server state needed in V1
Monorepopnpm workspacesClean shared package without npm publish
DeploymentCloudflare PagesFree, global CDN, static export
CIGitHub ActionsAuto-deploy on push to main

Getting started

Prerequisites

  • Node.js 20+
  • pnpm 9+
# Install pnpm if you don't have it
npm install -g pnpm

Installation

# 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 dev

Open http://localhost:3000.

Building for production

# Build everything
pnpm build
# Preview the static export locally
npx serve apps/dashboard/out -p 3001

Using @signalflow/shared as a library

The core SDP logic is in a separate package and can be used independently:

Parse an SDP

import{parseSDP}from'@signalflow/shared';constsdp=parseSDP(`v=0o=- 1234 2 IN IP4 127.0.0.1s=-t=0 0a=group:BUNDLE 0m=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'

Diff two SDPs

import{parseSDP,diffSDPs}from'@signalflow/shared';constoffer=parseSDP(offerString);constanswer=parseSDP(answerString);constresult=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}`);});

Run diagnostics

import{parseSDP,runDiagnostics}from'@signalflow/shared';constoffer=parseSDP(offerString);constanswer=parseSDP(answerString);constissues=runDiagnostics(offer,answer);issues.forEach(issue=>{console.log(`[${issue.severity}] ${issue.title}`);console.log(` Why: ${issue.explanation}`);console.log(` Fix: ${issue.fix}`);});

Detect browser/SDK source

import{parseSDP}from'@signalflow/shared';constsdp=parseSDP(rawSdpString);console.log(sdp.source);// 'Chrome' | 'Firefox' | 'Safari' | 'LiveKit' | ...

How to get your SDP strings

From your own app (browser console)

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);};

From chrome://webrtc-internals

  1. Open chrome://webrtc-internals in a new tab
  2. Start your WebRTC call in another tab
  3. Find your PeerConnection entry
  4. Expand "setLocalDescription" or "setRemoteDescription"
  5. Copy the sdp field value

From your server logs

If your signaling server logs SDP exchanges (it should), grep for "type":"offer" and "type":"answer" entries.


Running tests

# Build shared packagecd 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.mjs

Expected 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

Deployment

Cloudflare Pages (recommended)

# One-time setup
npm install -g wrangler
wrangler login
# Deploy
pnpm deploy

The deploy script builds the Next.js static export and pushes to Cloudflare Pages automatically.

GitHub Actions (auto-deploy)

Add these secrets to your repository:

SecretValue
CF_API_TOKENCloudflare API token with Pages:Edit permission

Every push to main triggers a build and deploy automatically.

Other platforms

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.

Why not use existing tools?

ToolWhat it doesWhat it can't do
chrome://webrtc-internalsShows raw WebRTC statsNo diff, no explanation, not shareable
Postman / InsomniaTest WebSocket messagesNo WebRTC awareness at all
WiresharkDeep packet inspectionNo SDP semantics, no diagnosis
rtcStatsCollects getStats() dataNo signaling layer, no diff, rule-based only
diff / VSCode diffText comparisonLine-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.


Roadmap

V2 — SDK + live capture

  • npm install @signalflow/sdk — one-line drop-in that wraps RTCPeerConnection and 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)

V3 — AI + chaos

  • 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

Contributing

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

Adding a diagnostic rule

  1. Identify which category it belongs to: ice, dtls, codecs, simulcast, or bundle
  2. Add your rule function to the relevant file in packages/shared/src/diagnostics/
  3. The function signature is: (sdp1: ParsedSDP, sdp2: ParsedSDP) => DiagnosticIssue | null
  4. Return null if the condition is NOT met — no false positives
  5. Write the explanation as 2-3 sentences a junior developer can understand
  6. Write the fix as a concrete action, not a vague suggestion
  7. Add a test SDP that triggers your rule to the test suite
// Example: new rule in packages/shared/src/diagnostics/ice.tsexportconstmyNewRule: DiagnosticRule=(sdp1,sdp2)=>{// return null if condition not metif(conditionNotMet)returnnull;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.',};};

Development setup

git clone https://github.com/yourusername/signalflow.git
cd signalflow
pnpm install
pnpm --filter @signalflow/shared build
pnpm dev

The dev server hot-reloads. Changes to packages/shared require a rebuild:

# In a separate terminalcd packages/shared && pnpm build --watch

License

MIT — 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

Acknowledgements

  • 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

About

The missing debugger for WebRTC + WebSocket developers.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages