TURN Fails on iOS/WebView? Verify Credentials and That You’re Actually Using Relay
Published: 2026-02-18
webrtc
turn
coturn
authentication
ios
webview
troubleshooting
If desktop works but iOS (or an in-app WebView) fails, validate two basics before changing anything else: (1) you can actually produce relay candidates, and (2) the TURN username/credential you send matches what the server expects.
Read checklist + pseudo-code
Checklist
- Force relay once: temporarily set
iceTransportPolicy: 'relay' to confirm it’s TURN-related.
- Confirm relay candidates: log ICE candidate strings and look for
typ relay.
- Do not mix credential types: static user/pass vs time-limited (TURN REST) credentials must match your server config.
- Check clock skew: for time-limited credentials, client/server time drift can break authentication.
- A/B browser vs WebView: if only WebView fails, test TURN over TCP/TLS as a diagnostic fallback (some environments treat UDP differently).
Pseudo-code: force relay + log candidate type (standard WebRTC API)
// PSEUDO-CODE (not WLSDK)
const pc = new RTCPeerConnection({
iceServers: [
{
urls: 'turn:turn.example.com:3478',
username: '<USERNAME>',
credential: '<CREDENTIAL>',
},
],
iceTransportPolicy: 'relay',
});
pc.onicecandidate = (e) => {
if (!e.candidate) return;
const c = e.candidate.candidate;
if (c.includes(' typ relay ')) console.log('relay', c);
};
Embedding live streaming into a product? Start from the PHP SDK Integration Guide. Need developer access? Apply for SDK.
No srflx Candidates on Cloud/PaaS? Why STUN Works Locally but Not in Production
Published: 2026-02-18
webrtc
ice
stun
srflx
cloud
paas
deployment
troubleshooting
If you can collect srflx candidates locally but never in production, treat it as a network egress / platform restriction problem until proven otherwise. First: collect evidence by logging candidate types.
Read checklist + pseudo-code
Checklist
- Log all candidates: confirm whether you only see
host candidates, or also srflx/relay.
- Validate UDP egress: security groups / egress firewall rules can silently block STUN traffic.
- Use TURN as a control: if
relay works but srflx never appears, STUN is the suspect path.
- Minimize the repro: a tiny page that only gathers candidates (same deployment) is easier to reason about.
Pseudo-code: log ICE candidate types (standard WebRTC API)
// PSEUDO-CODE (not WLSDK)
pc.onicecandidate = (e) => {
if (!e.candidate) return;
const c = e.candidate.candidate;
if (c.includes(' typ host ')) console.log('host', c);
if (c.includes(' typ srflx ')) console.log('srflx', c);
if (c.includes(' typ relay ')) console.log('relay', c);
};
If you’re embedding live streaming into a product, keep your baseline reproducible. Start from the PHP SDK Integration Guide. Need developer access? Apply for SDK.
DTLS Error: no SRTP profile negotiated — Capture Offer/Answer Before You Guess
Published: 2026-02-18
webrtc
dtls
srtp
sdp
whip
ffmpeg
interoperability
troubleshooting
When you see no SRTP profile negotiated, the fastest move isn’t tweaking random parameters. Capture the offer/answer SDP first and verify the endpoint is actually negotiating DTLS-SRTP as a browser expects.
Read checklist + pseudo-code
Checklist
- Save both SDP blobs: keep the full offer and answer for the failing session.
- Verify fingerprints: check for
a=fingerprint and sensible a=setup roles.
- Watch out for SDP rewriting: proxies/SFUs/signaling code that edits SDP can accidentally strip required lines.
- Interoperability check: if bridging WHIP/FFmpeg/SFU components, confirm they speak DTLS-SRTP (not SDES-SRTP).
- Reduce variables: reproduce with a single audio or video m-line before adding complexity.
Pseudo-code: dump offer/answer SDP (standard WebRTC API)
// PSEUDO-CODE (not WLSDK)
function dump(label, desc) {
console.log(label, desc.type);
console.log(desc.sdp);
}
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
dump('local', pc.localDescription);
// after you receive `answer` from signaling
await pc.setRemoteDescription(answer);
dump('remote', pc.remoteDescription);
Embedding live streaming into a product? Start from the PHP SDK Integration Guide to keep your baseline clean. Need developer access? Apply for SDK.
Mobile WebRTC “No Sound”: Output Routing, setSinkId (When Supported), and WebView Pitfalls
Published: 2026-02-18
webrtc
audio
routing
devices
webview
ios
android
troubleshooting
On mobile, “no sound” is often not capture—it’s output routing or autoplay policy. Enumerate outputs, gate setSinkId (when supported), and A/B test browser vs in-app WebView.
Read checklist + pseudo-code
Checklist
- User gesture first: start playback after a click; double-check
muted and system volume.
- Enumerate devices: confirm you can see
audiooutput in enumerateDevices().
- Gate
setSinkId: don’t assume it exists (support varies by browser / WebView).
- Listen for changes: handle
devicechange (Bluetooth / headset events).
- Keep a minimal repro: one remote track + one
<audio> element.
Pseudo-code: enumerate outputs + setSinkId (when available)
// PSEUDO-CODE (not WLSDK)
async function listAudioOutputs() {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter((d) => d.kind === 'audiooutput');
}
async function setOutput(audioEl, deviceId) {
if (typeof audioEl.setSinkId !== 'function') {
console.warn('setSinkId not supported in this environment');
return;
}
await audioEl.setSinkId(deviceId);
}
If you’re embedding live streaming into a product, start from the PHP SDK Integration Guide and keep your baseline boring. Need developer access? Apply for SDK.
Connected but No Remote Video Until Reconnect? Fix Renegotiation (m-line order) with Perfect Negotiation
Published: 2026-02-04webrtcnegotiationrenegotiationsdptransceiverm-lineperfect-negotiationtroubleshooting
If the peer is “connected” but remote media only appears after a reconnect, suspect renegotiation: offer collision (glare) or drifting SDP m= (m-line) order—especially with transceivers.
Read checklist + pseudo-code
Checklist
- Log state changes: capture
signalingState/connectionState/iceConnectionState around renegotiation. - Diff SDP m-line order: compare the
m= section order between working and failing runs. - Stabilize transceivers: create audio/video transceivers once in a fixed order; prefer
replaceTrack(). - Serialize offers: avoid overlapping offers from multiple
negotiationneeded events. - Use Perfect Negotiation: handle offer collisions consistently (polite/impolite + ignoreOffer).
Pseudo-code: Perfect Negotiation (standard WebRTC API)
let makingOffer = false;
let ignoreOffer = false;
const polite = true;
pc.onnegotiationneeded = async () => {
try {
makingOffer = true;
await pc.setLocalDescription();
sendSignal({ description: pc.localDescription });
} finally {
makingOffer = false;
}
};
onSignal(async ({ description, candidate }) => {
if (description) {
const offerCollision =
description.type === 'offer' &&
(makingOffer || pc.signalingState !== 'stable');
ignoreOffer = !polite && offerCollision;
if (ignoreOffer) return;
await pc.setRemoteDescription(description);
if (description.type === 'offer') {
await pc.setLocalDescription();
sendSignal({ description: pc.localDescription });
}
} else if (candidate) {
try {
await pc.addIceCandidate(candidate);
} catch (e) {
if (!ignoreOffer) throw e;
}
}
});
Embedding live streaming into a product? Start from the PHP SDK Integration Guide. Need developer access? Apply for SDK.
DataChannel Freezes on Binary Chunks? Add Backpressure with bufferedAmount
Published: 2026-02-04webrtcdatachannelbufferedamountbackpressurefile-transfertroubleshootingnodejselectron
If DataChannel works for small messages but freezes on binary chunks (files), you’re likely missing backpressure: you’re pushing faster than the channel can drain, so bufferedAmount grows until the app stalls.
Read checklist + pseudo-code
Checklist
- Start small: try 16–64KB chunks first.
- Gate sends: throttle on
dc.bufferedAmount and bufferedamountlow. - Check state: send only when
dc.readyState === 'open'. - Receiver memory: process incrementally; avoid holding the full file in RAM.
Pseudo-code: bufferedAmount backpressure (standard WebRTC API)
const CHUNK = 64 * 1024;
async function sendFile(dc, buf) {
dc.bufferedAmountLowThreshold = 1024 * 1024;
for (let i = 0; i < buf.byteLength; i += CHUNK) {
while (dc.bufferedAmount > dc.bufferedAmountLowThreshold) {
await once(dc, 'bufferedamountlow');
}
dc.send(buf.slice(i, i + CHUNK));
}
}
function once(dc, evt) {
return new Promise((r) =>
dc.addEventListener(evt, () => r(), { once: true })
);
}
If you’re embedding live streaming into a product, keep your baseline reproducible. Start from the PHP SDK Integration Guide. Need developer access? Apply for SDK.
WebRTC H.264 Works in One Browser but Not Another? Check SDP fmtp (profile-level-id)
Published: 2026-02-02webrtch264sdpfmtpprofile-level-idinteroperabilitytroubleshooting
If one browser/device gets ontrack but shows black video (or plays audio only), don’t assume it’s ICE. With H.264, mismatched SDP fmtp parameters—especially profile-level-id—are a frequent interoperability trap.
Read the checklist
The symptom: track is there, decoding isn’t
This often appears as “works on Chrome, fails on Safari/iOS” or “works on desktop, fails on some Android devices”. The connection can be fully established while the decoder rejects the negotiated H.264 profile.
Checklist
- Capture both SDP blobs: save the offer and answer from the working and failing cases.
- Locate the H.264 payload type: find
a=rtpmap:<pt> H264/90000 and note the <pt>. - Compare
fmtp lines: check a=fmtp:<pt> ... for profile-level-id, packetization-mode, and level-asymmetry-allowed. - Make profiles compatible: if you control the encoder, align H.264 profiles across platforms (a quick A/B is switching to another codec like VP8 to confirm the issue is codec negotiation).
- Avoid stripping fmtp: proxies/SFUs/signaling layers that rewrite SDP can accidentally remove or mutate H.264 parameters.
When you’re embedding live streaming into a product, keep your reproduction steps minimal and consistent. Start from the PHP SDK Integration Guide and only add complexity once you can observe the negotiation differences. Need developer access? Apply for SDK.
Firefox Gets the Track but Video Is Black? Check mDNS ICE Candidates (.local)
Published: 2026-02-02webrtcfirefoxicemdnstroubleshooting
If your stream works in Chrome/Edge but Firefox shows a black video while the track exists, treat it as a connectivity (ICE) debugging problem first—especially if selected host candidates include .local mDNS names.
Read the checklist
The symptom: track exists, frames don’t
You might see ontrack fire, but the video element stays black. That can happen when the connection is not actually carrying RTP (or it’s connected to a candidate path that doesn’t work in your real network).
Checklist
- Confirm ICE state: log
iceConnectionState and capture a snapshot when it becomes connected/completed. - Inspect the selected candidate pair: in Firefox, use
about:webrtc to see which local/remote candidates were chosen. - Look for
.local in candidates: this is often an mDNS host candidate. On some LAN/AP setups, peers can’t resolve each other’s mDNS names, leading to “connected but no media”. - Hotspot A/B test: if a phone hotspot works but office Wi‑Fi fails, it’s almost always a network path / name resolution / firewall story.
- Force relay for a test: if you control the WebRTC layer, temporarily try relay-only (
iceTransportPolicy: 'relay') to confirm whether the problem disappears when using TURN relay paths.
If you’re embedding live streaming into a product, keep your integration baseline minimal and reproducible. Start from the PHP SDK Integration Guide and only add complexity after you can reproduce and observe the states. Need developer access? Apply for SDK.
Keep Player UI Language Consistent: Using the WLSDK lang Parameter
Published: 2026-01-31i18nlocalizationphpsdkiframe
If your site has multiple interface languages, pass lang when calling WLSDK::iframe() (or WLSDK::lazyIframe()) so the player language stays consistent with the page.
Read implementation code
What the Integration Guide documents
The Integration Guide lists the player supported language codes: en, ja, zh-Hant, zh-Hans, vi. When lang is not specified, WLSDK reads the execution environment language; if unavailable, it falls back to en.
Example: force English player UI
<?php
echo WLSDK::iframe([
'hostLabel' => 'connect',
'streamer' => '<STREAMER_USER_ID>',
'lang' => 'en',
]);
?>
See Integration Guide → “Multi-language integration (optional)”. If you need developer access, apply for the SDK.
Optimizing Multi-Stream Dashboards: Lazy Loading Live Video with PHP
Published: 2026-01-31performancelazy-loadingphpscalingdashboard
On a dashboard, initializing many live players at the same time can increase client CPU and bandwidth usage. Use WLSDK::lazyIframe() to defer stream initialization until you actually need it.
Read implementation code
Dashboards tend to load too much, too early
If your page renders many live views at once, you can end up initializing multiple players immediately on page load. That’s often unnecessary if most views are off-screen.
Use the documented “Multiple Iframes Lazy Loading” pattern
The Integration Guide includes a multi-iframe example using WLSDK::lazyIframe(). Keep the call minimal and only pass documented parameters.
<!-- Inside your loop -->
<?php foreach ($activeStreamers as $streamerId): ?>
<div class="stream-wrapper">
<?php
echo WLSDK::lazyIframe([
'hostLabel' => 'connect',
'streamer' => $streamerId,
]);
?>
</div>
<?php endforeach; ?>
For details, see Integration Guide → “Multiple Iframes Lazy Loading”.
Why Browser MediaRecorder is Not Production Ready
Published: 2025-12-28recordingarchitecturereliability
Using MediaRecorder in the browser seems simple, but in production, you face No seeking (missing metadata), Crash risk (RAM loss), and Variable Frame Rate sync issues.
Read full technical teardown
The "easy way" that breaks later
Using MediaRecorder in the browser seems simple: record chunks, save Blob. But in production, you face:
- No seeking: Resulting WebM files often lack metadata (duration/seek table).
- Crash risk: If the tab crashes or user refreshes, the entire recording in RAM is lost.
- Variable Frame Rate (VFR): Browser encoders drop frames under load, causing A/V sync issues in post-production.
The Reliable Solution: Server-Side Recording
Composite and record on the server where you control the CPU and network. If recording/export (e.g., MP4) is a requirement, plan it as a server-side pipeline and confirm supported options when you apply for the SDK.
Handling Network Handover (WiFi ↔ 4G) in WebRTC
Published: 2025-12-28mobileconnectivityice-restart
When a user walks out of WiFi range, their IP changes and the UDP socket dies. The fix is ICE Restart: detecting the disconnect and triggering pc.createOffer({ iceRestart: true }) to re-negotiate without destroying the stream.
Read full implementation guide
The "Frozen Video" Symptom
When a user walks out of WiFi range, their IP changes. The existing UDP socket dies. The video freezes.
The Fix: ICE Restart
Monitor iceConnectionState. When it hits disconnected or failed, trigger an ICE Restart (re-negotiate with iceRestart: true).
pc.createOffer({ iceRestart: true })
This forces the browser to gather new candidates on the new 4G network without destroying the media stream.
If you're shipping an embedded live experience, treat reconnect as part of your own state machine (ICE restart + UI). Start from the Integration Guide and tailor it to your app.
ICE Failed / TURN Connectivity Troubleshooting Checklist
Published: 2025-12-25webrtciceturntroubleshooting
ICE Connection Failed is usually a firewall/NAT issue. Configure a TURN server (UDP 3478) and keep TCP 443 (TURNS) as a fallback for corporate networks and Symmetric NAT.
Read full checklist
Symptoms and quick checks
If calls fail with ICE Connection Failed or get stuck in candidate gathering, assume the network path (firewall/NAT) or TURN config is the culprit.
Baseline configuration
- STUN/TURN: Always provide a TURN server; don’t rely on STUN-only configs.
- UDP 3478: Standard TURN port. Corporate networks often block UDP.
- TCP 443 fallback: When UDP is blocked, make sure TURN supports TCP 443 (TURNS).
Symmetric NAT is a P2P killer
With Symmetric NAT, direct peer-to-peer often won’t work—you must relay via TURN.
If operating TURN isn’t your core business, consider a managed TURN provider—and when evaluating a vendor (or us when you apply for the SDK), ask what networking components are included in your plan.
How to Embed Live Streaming in Any App (Quickstart)
Published: 2025-12-25integrationiframesdkjavascript
For a secure embed, generate the iframe server-side so long-lived credentials never reach the browser. Start from WLSDK::setup() + WLSDK::iframe() in the Integration Guide.
Read integration code
The Practical Way: Generate the embed on your backend
Avoid hardcoding player URLs or credentials in frontend code. Generate the embed HTML on your backend and serve it to authenticated users; this keeps secrets server-side and gives you a single place to enforce your own access rules.
<?php
// Include WebLiveHub SDK class
require_once __DIR__ . '/vendor/autoload.php';
use WebLiveHub\SDK\WLSDK;
// Initialize SDK configuration
WLSDK::setup([
'hb_endpoint' => getenv('WL_HOSTED_BACKEND_URL') ?: '<HOSTED_BACKEND_ENDPOINT>',
'user_id' => '<AUTH_USER_ID>',
'password' => '<AUTH_PASSWORD>'
]);
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<?php echo WLSDK::script(); ?>
</head>
<body>
<?php
echo WLSDK::iframe([
'hostLabel' => 'connect',
'streamer' => '<STREAMER_USER_ID>',
'attrs' => [ 'id' => 'stream-container', 'class' => 'wlh-stream', 'style' => 'border-radius:6px;' ]
]);
?>
</body>
</html>
The SDK outputs an <iframe> embed. Use attrs to pass standard HTML attributes, and validate autoplay/permission behavior in your target browsers (especially iOS/Safari).
Why use the SDK?
- Backend-first integration: Keep long-lived credentials on your server; decide when to render an embed for a logged-in user.
- Solid baseline: Follow the Integration Guide (setup → script → iframe) and then extend from there.
👉 Get your SDK Credentials
WebRTC Integration Preflight Checklist: Make the Environment Boring Before You Debug
Published: 2025-12-22webrtcintegrationchecklistcompatibility
“No video / no audio / black screen” is often an environment mismatch, not a platform outage. Validate your baseline first with Chrome, HTTPS, and network checks.
Read more
Baseline first. Debug second.
“No video / no audio / black screen” is often an environment mismatch, not a platform outage. Normalize your baseline so your debugging is actually meaningful.
Browser & device
- Validate in latest Chrome/Edge first (then Safari/iOS).
- Confirm camera/mic permissions and that devices exist.
- Try an incognito window to rule out extensions.
Deployment & network
- HTTPS is required (local
localhost is OK). - Corporate networks may block UDP—compare with a phone hotspot.
Playback
- Assume autoplay is blocked: start after a click.
- Verify output device, mute state, and system volume.
Next: run the minimal flow in the PHP SDK Integration Guide. Need access? Apply for SDK.
No Audio / No Video in WebRTC: A Repeatable Troubleshooting Path
Published: 2025-12-22webrtctroubleshootingaudiovideodiagnostics
Stop guessing. Isolate the failure domain into Media (permissions/hardware), Connectivity (network/firewall), or Playback (autoplay/mute). Capture observable states.
Read more
Split the problem: media → connectivity → playback
Stop guessing. Turn “it doesn’t work” into observable states.
1) Media
- Are permissions granted? Are camera/mic devices available?
- Test another device to rule out hardware/driver issues.
2) Connectivity
- Hotspot test is your fastest signal: if hotspot works, the network policy is the culprit.
3) Playback
- Start after a click to avoid autoplay/mute traps.
- Double-check the output device (Bluetooth / monitor / speakers).
Capture these once
- Chrome:
chrome://webrtc-internals export or screenshots. - Timestamp, browser version, OS, network type.
Next: validate your flow against the PHP SDK Integration Guide.
Safari / iOS WebRTC Pitfalls: Permissions, Backgrounding, and Autoplay
Published: 2025-12-22webrtcsafariioscompatibilitytroubleshooting
iOS is strictly different from Desktop Chrome. Background execution, autoplay policies, and permission management require specific handling. Test with user interaction first.
Read more
Why “Chrome works, iPhone doesn’t” keeps happening
On iOS, permission and foreground/background behavior is stricter. Your test plan must be explicit.
Suggested test checklist
- Always start after a user click.
- Test background → foreground recovery.
- Repeat 3 times: intermittent issues show up with repetition.
Include in bug reports
- iPhone model, iOS version, Safari version
- Bluetooth audio usage
- Low Power Mode
Next: establish an iOS baseline using the PHP SDK Integration Guide, then layer features.
Measuring Stream Quality and Latency: The Metrics That Actually Matter
Published: 2025-12-22webrtcmetricslatencyqualityobservability
“It’s laggy” is not a metric. You must measure Packet Loss, Jitter, Bitrate, and Decode Time to truly understand streaming quality and troubleshoot issues.
Read more
“It’s laggy” is not a metric
Stutter is usually a blend of packet loss, jitter, insufficient bitrate, or decode pressure on the device.
5 metrics to watch
- Packet loss
- Jitter
- Bitrate
- Frames dropped / decode time
- End-to-end latency
How to capture
- Chrome:
chrome://webrtc-internals for quick trends. - Run the same scenario 3 times before comparing.
Next: complete the PHP SDK Integration Guide and define your quality baseline.
SPA Lifecycle for Embedded Live: Mount, Unmount, Reconnect Without Leaks
Published: 2025-12-22frontendSPAlifecyclereliabilitytroubleshooting
The classic SPA bug: if you don’t clean up events/timers on unmount, you’ll see duplicate sessions and memory growth. Use a lifecycle-aware state machine.
Read more
The classic SPA bug: the page changed, but the listeners didn’t
If you don’t clean up events/timers, you’ll see duplicate sessions, audio device contention, or memory growth.
A reliable rule
- Register on mount.
- Unregister on unmount (removeEventListener, clearInterval, release resources).
- Reconnect via a state machine to avoid concurrent reconnect storms.
Useful events
visibilitychange (background/foreground)pagehide (common on mobile)
Next: align your lifecycle with the PHP SDK Integration Guide.
No posts match your filters.