<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Maxwell Lin</title>
    <description>The latest articles on DEV Community by Maxwell Lin (@maxwell_lin_d59a2746f8a27).</description>
    <link>https://dev.to/maxwell_lin_d59a2746f8a27</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3079506%2Fa8e0d21c-dbc5-4b9d-bb37-40b3e051b1a2.png</url>
      <title>DEV Community: Maxwell Lin</title>
      <link>https://dev.to/maxwell_lin_d59a2746f8a27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maxwell_lin_d59a2746f8a27"/>
    <language>en</language>
    <item>
      <title>WebRTC vs HLS for live streaming: latency, interaction and when to use each</title>
      <dc:creator>Maxwell Lin</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:28:55 +0000</pubDate>
      <link>https://dev.to/maxwell_lin_d59a2746f8a27/webrtc-vs-hls-for-live-streaming-latency-interaction-and-when-to-use-each-1cd8</link>
      <guid>https://dev.to/maxwell_lin_d59a2746f8a27/webrtc-vs-hls-for-live-streaming-latency-interaction-and-when-to-use-each-1cd8</guid>
      <description>&lt;p&gt;&lt;em&gt;From the Streaming CDN team. The original, kept up to date, is at &lt;a href="https://streaming-cdn.ewin888.com/guides/webrtc-vs-hls-live-streaming-latency" rel="noopener noreferrer"&gt;streaming-cdn.ewin888.com/guides/webrtc-vs-hls-live-streaming-latency&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;HLS and DASH cut a live stream into segments of a few seconds and the player buffers several of them before it starts, so viewers usually watch several seconds or more behind the camera. Low-latency variants shrink that, but the design is still built on files and buffers. WebRTC sends media continuously as packets over a real-time connection, so end-to-end delay is measured in hundreds of milliseconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HLS / DASH&lt;/th&gt;
&lt;th&gt;WebRTC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typical delay&lt;/td&gt;
&lt;td&gt;Several seconds and more&lt;/td&gt;
&lt;td&gt;Low hundreds of milliseconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How media travels&lt;/td&gt;
&lt;td&gt;Segment files over HTTP, buffered&lt;/td&gt;
&lt;td&gt;Continuous packets over a real-time connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser playback&lt;/td&gt;
&lt;td&gt;Through a player library (native in Safari)&lt;/td&gt;
&lt;td&gt;Native in every modern browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interaction in the same moment&lt;/td&gt;
&lt;td&gt;Viewers react to what happened seconds ago&lt;/td&gt;
&lt;td&gt;Host and viewers interact in the same second&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weak networks&lt;/td&gt;
&lt;td&gt;Switches renditions between segments&lt;/td&gt;
&lt;td&gt;Simulcast: several quality levels at once, each viewer gets what their line can carry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  When the seconds matter
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Live chat, Q&amp;amp;A and votes: a host answering a question that is already ten seconds old breaks the conversation.&lt;/li&gt;
&lt;li&gt;Live commerce and auctions: bids and flash-sale stock have to match the picture.&lt;/li&gt;
&lt;li&gt;Sports and esports: with in-play predictions or bets, a viewer who is seconds behind learns the result from a faster source first.&lt;/li&gt;
&lt;li&gt;Remote monitoring: operators watching vehicles, tables or sites need the picture as it is now.&lt;/li&gt;
&lt;li&gt;Classes and coaching: a teacher's question and a student's answer must meet in real time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When HLS is still fine
&lt;/h2&gt;

&lt;p&gt;One-way broadcasts where nobody interacts, and on-demand video, do not gain much from shaving seconds; plain HTTP delivery and long caches work well there.&lt;/p&gt;

&lt;h2&gt;
  
  
  WebRTC delivery without building it
&lt;/h2&gt;

&lt;p&gt;Running WebRTC at scale means media servers, TURN relays, simulcast, reconnects and a delivery network close to viewers. Streaming CDN does that part:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Publish from OBS over WHIP, RTMP or SRT encoders, or relay an existing RTSP, RTMP or HTTP-FLV stream.&lt;/li&gt;
&lt;li&gt;Viewers watch over WebRTC in the browser, through a network in more than 130 countries and territories, with nothing to install.&lt;/li&gt;
&lt;li&gt;Chat, votes and live data travel on real-time data channels in step with the picture.&lt;/li&gt;
&lt;li&gt;Signed playback URLs, Referer/Origin allowlists and request-source policies keep the stream on your own pages.&lt;/li&gt;
&lt;li&gt;Usage is measured per customer, stream and domain for billing.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Streaming CDN delivers live video to browsers over WebRTC, with chat and votes on real-time data channels, adaptive quality, recording and playback protection. &lt;a href="https://streaming-cdn.ewin888.com/products/live-streaming" rel="noopener noreferrer"&gt;Learn more&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>livestreaming</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Native incoming calls in React Native: CallKit on iOS, full-screen calls on Android</title>
      <dc:creator>Maxwell Lin</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:28:34 +0000</pubDate>
      <link>https://dev.to/maxwell_lin_d59a2746f8a27/native-incoming-calls-in-react-native-callkit-on-ios-full-screen-calls-on-android-671</link>
      <guid>https://dev.to/maxwell_lin_d59a2746f8a27/native-incoming-calls-in-react-native-callkit-on-ios-full-screen-calls-on-android-671</guid>
      <description>&lt;p&gt;&lt;em&gt;From the Streaming CDN team. The original, kept up to date, is at &lt;a href="https://streaming-cdn.ewin888.com/guides/react-native-incoming-call-screen" rel="noopener noreferrer"&gt;streaming-cdn.ewin888.com/guides/react-native-incoming-call-screen&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A calling app has to ring when it is not running: on a locked iPhone, on an Android phone in a pocket, after the system has reclaimed the process. That takes a push that wakes the app and a system incoming-call screen, on each platform in its own way. This guide shows how the Streaming CDN calling SDK does it for React Native and Expo.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a call reaches a sleeping phone
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The caller starts a call; the call service sends a push with the call id, caller and mode (audio or video).&lt;/li&gt;
&lt;li&gt;iOS: a VoIP push (PushKit) wakes the app, which reports the call to CallKit. Android: an FCM message wakes the app, which shows a full-screen incoming-call notification.&lt;/li&gt;
&lt;li&gt;The person answers from the lock screen. The app accepts the call with its client token.&lt;/li&gt;
&lt;li&gt;The service makes the accept atomic — the first device to answer wins — and returns the media credential to that device only. Every other device signed in as the same person stops ringing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The push itself carries no media token and no server key; the media credential is returned only after the accept succeeds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"rtc_call_..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"callerName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alex"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"video"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-26T04:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @streaming-cdn/rtc-react-native react-native-webrtc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Bare React Native apps rebuild the native project as usual.&lt;/li&gt;
&lt;li&gt;Expo apps need &lt;code&gt;expo prebuild&lt;/code&gt; and a development build or EAS Build — Expo Go cannot load a native WebRTC runtime. Add &lt;code&gt;@streaming-cdn/rtc-react-native&lt;/code&gt; to the Expo &lt;code&gt;plugins&lt;/code&gt; list so the camera and microphone usage descriptions are generated.&lt;/li&gt;
&lt;li&gt;Upgrading from a version without the native call layer means rebuilding the native project; until then native calls report that the module is unavailable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Join a call
&lt;/h2&gt;

&lt;p&gt;Your backend keeps the server API key and hands the app a short-lived credential:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactNativeRtcClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@streaming-cdn/rtc-react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-backend.example.com/api/rtc/identity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rtc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReactNativeRtcClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;video&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publishAudio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publishVideo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;receiveAudio&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;receiveVideo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What the native call layer gives you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;Platform behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Locked-screen and background calls&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RtcNativeIncomingCalls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Android full-screen intent notification; iOS PushKit with CallKit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ringtone and ringback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RtcCallTones&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Honours the Android ringer mode; on iOS CallKit owns the incoming ringtone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio output routing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;client.setAudioOutput()&lt;/code&gt;, &lt;code&gt;RtcAudioRouting&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Earpiece, speaker and Bluetooth through Android &lt;code&gt;AudioManager&lt;/code&gt; and iOS &lt;code&gt;AVAudioSession&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On iOS a call identifier must be a UUID. On Android your app owns its Firebase registration and forwards payloads to the SDK, so Firebase stays an optional dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Android 14 trap: full-screen intent
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;USE_FULL_SCREEN_INTENT&lt;/code&gt; is what lets an incoming call take over the screen. Three facts to know before shipping:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Since Android 14 (API 34) it is a special app access, granted automatically only to apps whose core function is alarms or calls. Other apps start without it.&lt;/li&gt;
&lt;li&gt;Since 31 May 2024, Google Play requires a declaration for it in the Play Console (App content → Sensitive permissions). Submit it before review.&lt;/li&gt;
&lt;li&gt;When the grant is missing there is no error: the call silently becomes an ordinary heads-up notification that is easy to miss. A build can pass testing and still be effectively uncallable on real devices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Check at runtime and guide the user before the first call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;caps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RtcNativeIncomingCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getIncomingCallCapabilities&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// caps.presentation: "full-screen" | "notification" | "none"&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;presentation&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;full-screen&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// explain why, then:&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;RtcNativeIncomingCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openFullScreenIntentSettings&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;getIncomingCallCapabilities()&lt;/code&gt; also reports &lt;code&gt;notificationsEnabled&lt;/code&gt;; when it is false the call will not appear at all. On iOS the result is always full-screen, because CallKit owns presentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ringing ends on its own
&lt;/h2&gt;

&lt;p&gt;A ringing invitation is valid for 45 seconds and a call nobody answers is closed as missed. On Android the incoming-call notification also carries a system timeout (45 seconds by default, &lt;code&gt;ringTimeoutMs&lt;/code&gt; in the push payload, 10–120 seconds), so ringing stops even if the app process was reclaimed and no cancel can run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Errors worth handling
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The client identity token expired&lt;/td&gt;
&lt;td&gt;Refresh it through your backend&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;This identity is not a recipient of the call&lt;/td&gt;
&lt;td&gt;Do not show the call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;409&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Another device answered, or the call stopped ringing&lt;/td&gt;
&lt;td&gt;End the incoming UI quietly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Push after &lt;code&gt;expiresAt&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The call is over&lt;/td&gt;
&lt;td&gt;Discard it without showing any UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;&lt;em&gt;The Streaming CDN calling SDK brings voice and video calls with system incoming-call screens to iOS, Android, Web, React Native and Expo. &lt;a href="https://streaming-cdn.ewin888.com/products/rtc-sdk" rel="noopener noreferrer"&gt;Learn more&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>android</category>
      <category>ios</category>
      <category>webrtc</category>
    </item>
    <item>
      <title>Go live from OBS to the browser in under a second with WHIP</title>
      <dc:creator>Maxwell Lin</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:28:12 +0000</pubDate>
      <link>https://dev.to/maxwell_lin_d59a2746f8a27/go-live-from-obs-to-the-browser-in-under-a-second-with-whip-5bob</link>
      <guid>https://dev.to/maxwell_lin_d59a2746f8a27/go-live-from-obs-to-the-browser-in-under-a-second-with-whip-5bob</guid>
      <description>&lt;p&gt;&lt;em&gt;From the Streaming CDN team. The original, kept up to date, is at &lt;a href="https://streaming-cdn.ewin888.com/guides/obs-whip-webrtc-live-streaming" rel="noopener noreferrer"&gt;streaming-cdn.ewin888.com/guides/obs-whip-webrtc-live-streaming&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;WHIP (WebRTC-HTTP Ingestion Protocol) lets an encoder publish over WebRTC with a single HTTP request. OBS Studio supports it natively, so a stream can go from OBS to viewers' browsers over WebRTC end to end, in the low hundreds of milliseconds, instead of the several seconds that segment-based delivery such as HLS adds.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The publish URL
&lt;/h2&gt;

&lt;p&gt;OBS publishes to your ingest domain, a publish point and a stream name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://&amp;lt;ingest-hostname&amp;gt;/&amp;lt;publish-point&amp;gt;/&amp;lt;stream-name&amp;gt;

# example
https://push.example.com/live/camera-01
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The publish point defaults to &lt;code&gt;/live&lt;/code&gt;; the value configured in the console is authoritative.&lt;/li&gt;
&lt;li&gt;The stream name is the last path segment and is case-sensitive. Streams published at the same time on one ingest domain need different names.&lt;/li&gt;
&lt;li&gt;Viewers watch through a playback domain bound to the ingest domain the stream was published to.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Before you publish
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The ingest domain has active DNS and TLS.&lt;/li&gt;
&lt;li&gt;If source IP allowlisting is on, add the publisher's public egress IP (a private LAN address is not enough).&lt;/li&gt;
&lt;li&gt;If Stream Key authentication is on, create a key for this exact ingest domain. The secret is shown once.&lt;/li&gt;
&lt;li&gt;Use a current OBS Studio release that includes WHIP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3. OBS settings
&lt;/h2&gt;

&lt;p&gt;Open Settings → Stream and enter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OBS field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;WHIP&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;The full WHIP URL, including publish point and stream name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bearer Token&lt;/td&gt;
&lt;td&gt;The ingest domain's Stream Key (empty only when Stream Key authentication is off)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OBS sends the Bearer Token as &lt;code&gt;Authorization: Bearer &amp;lt;stream-key&amp;gt;&lt;/code&gt;. Treat Stream Keys as passwords: never put them in public URLs, screenshots, repositories or shared OBS profiles.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Recommended output for low latency
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Video encoder&lt;/td&gt;
&lt;td&gt;H.264 (hardware or software)&lt;/td&gt;
&lt;td&gt;Broad browser support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate control&lt;/td&gt;
&lt;td&gt;CBR&lt;/td&gt;
&lt;td&gt;Predictable bandwidth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;720p bitrate&lt;/td&gt;
&lt;td&gt;1.5–2.5 Mbps&lt;/td&gt;
&lt;td&gt;Adjust for motion and source quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1080p bitrate&lt;/td&gt;
&lt;td&gt;3–6 Mbps&lt;/td&gt;
&lt;td&gt;Adjust for motion and source quality&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keyframe interval&lt;/td&gt;
&lt;td&gt;1 second&lt;/td&gt;
&lt;td&gt;Faster first frame and recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;H.264 profile&lt;/td&gt;
&lt;td&gt;Main or Baseline&lt;/td&gt;
&lt;td&gt;Avoids unsupported decoder features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B-frames&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Lower latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audio&lt;/td&gt;
&lt;td&gt;Enabled, not muted&lt;/td&gt;
&lt;td&gt;WHIP negotiates the audio track&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  5. Check that it works
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;OBS changes from Connecting to Live.&lt;/li&gt;
&lt;li&gt;The console's Live streams list shows the stream name with active tracks.&lt;/li&gt;
&lt;li&gt;Open &lt;code&gt;https://&amp;lt;playback-hostname&amp;gt;/&amp;lt;publish-point&amp;gt;/&amp;lt;stream-name&amp;gt;&lt;/code&gt; and check video and audio.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  6. When it does not work
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;What to do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401 invalid_stream_key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Missing, expired, revoked or wrong-domain key&lt;/td&gt;
&lt;td&gt;Create a key for the active ingest domain and paste it into Bearer Token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403 publisher_ip_denied&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The publisher's public IP is not allowed&lt;/td&gt;
&lt;td&gt;Add the real public egress IP, or turn the allowlist off while testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;429 Too Many Requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A duplicate publisher, or a concurrency limit&lt;/td&gt;
&lt;td&gt;Stop the old publisher or use a different stream name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OBS stays on Connecting&lt;/td&gt;
&lt;td&gt;Firewall, outbound UDP/DTLS, DNS or TLS&lt;/td&gt;
&lt;td&gt;Check DNS/TLS, allow outbound UDP, read the OBS log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live with no picture&lt;/td&gt;
&lt;td&gt;Hidden or muted source, incompatible output&lt;/td&gt;
&lt;td&gt;Check the OBS preview and mixer; use H.264 and a one-second keyframe interval&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Publishing from a web page instead
&lt;/h2&gt;

&lt;p&gt;A browser page can publish too: WHIP with &lt;code&gt;fetch()&lt;/code&gt; and a send-only &lt;code&gt;RTCPeerConnection&lt;/code&gt;. Because that request is cross-origin, list the page's origin under the ingest domain's browser publishing origins (CORS) in the console. Encoders such as OBS never ask for CORS and are unaffected.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Streaming CDN delivers live video to browsers over WebRTC, with chat and votes on real-time data channels, adaptive quality, recording and playback protection. &lt;a href="https://streaming-cdn.ewin888.com/products/live-streaming" rel="noopener noreferrer"&gt;Learn more&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webrtc</category>
      <category>livestreaming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to stop other sites from re-streaming your live video</title>
      <dc:creator>Maxwell Lin</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:27:41 +0000</pubDate>
      <link>https://dev.to/maxwell_lin_d59a2746f8a27/how-to-stop-other-sites-from-re-streaming-your-live-video-ho</link>
      <guid>https://dev.to/maxwell_lin_d59a2746f8a27/how-to-stop-other-sites-from-re-streaming-your-live-video-ho</guid>
      <description>&lt;p&gt;&lt;em&gt;From the Streaming CDN team. The original, kept up to date, is at &lt;a href="https://streaming-cdn.ewin888.com/guides/stop-live-stream-hotlinking" rel="noopener noreferrer"&gt;streaming-cdn.ewin888.com/guides/stop-live-stream-hotlinking&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Once a live stream's URL is public, anyone can embed it on their own site, or run a scraper in a data center that re-streams it, and you pay for the traffic. No single setting stops every case; the answer is layers, each cheap to apply and each closing a different gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Signed playback URLs
&lt;/h2&gt;

&lt;p&gt;Your backend signs the playback URL with an expiry. A copied link stops working once it expires, so a URL pasted into a forum or another site is only good for a short time. Signed URLs only control who can start playback; a viewer who is already watching is not cut off.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Referer and Origin allowlists
&lt;/h2&gt;

&lt;p&gt;List the pages allowed to embed the player. Requests from any other site's page are refused, which stops the most common case: someone embedding your player or stream on their own page.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Request-source policies
&lt;/h2&gt;

&lt;p&gt;Real viewers come from home and mobile networks; most re-streaming scrapers run in data centers. A request-source policy classifies each request by the network it comes from and lets you log or refuse data-center sources per playback domain. Start in log mode, look at what it would have refused, then turn on refusal.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Address blocking
&lt;/h2&gt;

&lt;p&gt;When a specific source keeps pulling your stream, block its addresses. Request logs record every playback and publish, and can be exported as CSV to find the source.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Protect ingest too
&lt;/h2&gt;

&lt;p&gt;Protection is not only for viewers. Require a Stream Key on each ingest domain, and allowlist the publishers' public IP addresses where they are fixed, so nobody can publish into your channel.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to do
&lt;/h2&gt;

&lt;p&gt;Do not cap how many times or how long legitimate viewers may watch to fight abuse: that turns away the audience you are paying to reach. Use the layers above, which target the abusers, not the viewers.&lt;/p&gt;

&lt;h2&gt;
  
  
  On Streaming CDN
&lt;/h2&gt;

&lt;p&gt;Every customer sets these on its own playback and ingest domains in the console: signed URLs, Referer/Origin allowlists, request-source policies, address blocking, Stream Keys and publisher IP allowlists.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Streaming CDN delivers live video to browsers over WebRTC, with chat and votes on real-time data channels, adaptive quality, recording and playback protection. &lt;a href="https://streaming-cdn.ewin888.com/products/live-streaming" rel="noopener noreferrer"&gt;Learn more&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>livestreaming</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
