🚀 Developer Tech Blog

WebLiveHub Technical Insights

Deep dives into WebRTC, live streaming architecture, and SDK integration—built for developers shipping real products.

📖 Technical Articles

Explore the depth and breadth of live streaming tech

🏷️ Popular Tags

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-04
webrtcnegotiationrenegotiationsdptransceiverm-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-04
webrtcdatachannelbufferedamountbackpressurefile-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-02
webrtch264sdpfmtpprofile-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-02
webrtcfirefoxicemdnstroubleshooting

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-31
i18nlocalizationphpsdkiframe

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-31
performancelazy-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-28
recordingarchitecturereliability

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-28
mobileconnectivityice-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-25
webrtciceturntroubleshooting

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-25
integrationiframesdkjavascript

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-22
webrtcintegrationchecklistcompatibility

“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-22
webrtctroubleshootingaudiovideodiagnostics

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-22
webrtcsafariioscompatibilitytroubleshooting

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-22
webrtcmetricslatencyqualityobservability

“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-22
frontendSPAlifecyclereliabilitytroubleshooting

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.

What this blog covers

This blog focuses on the engineering side of building and embedding live streaming experiences with WebLiveHub.

  • Integration preflight (HTTPS, permissions, compatibility)
  • Troubleshooting (no audio/video, unstable connections, Safari/iOS)
  • Quality & latency metrics (loss, jitter, bitrate, end-to-end latency)
  • Production operations (report templates, diagnostic artifacts, support collaboration)

Explore WebLiveHub

See pricing and follow the integration guide to embed live streaming into your product.

View Pricing