<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[First Principles]]></title><description><![CDATA[Engineer at heart. Solves problems for fun. Exploring WebRTC and Distributed systems. Building cool stuff @100mslive.]]></description><link>https://blog.tushartripathi.me</link><generator>RSS for Node</generator><lastBuildDate>Sat, 09 May 2026 00:38:01 GMT</lastBuildDate><atom:link href="https://blog.tushartripathi.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Adding AR Filters in a 100ms Video Call - Part 1]]></title><description><![CDATA[How cool would it be if you could build your own Video Call app with Snapchat-like filters in it!

Ikr! That's what I was thinking when I came across Jeeliz. Now I have worked with tensorflow.js based libraries in the past but they're usually quite C...]]></description><link>https://blog.tushartripathi.me/adding-ar-filters-in-a-100ms-video-call-part-1</link><guid isPermaLink="true">https://blog.tushartripathi.me/adding-ar-filters-in-a-100ms-video-call-part-1</guid><category><![CDATA[WebRTC]]></category><category><![CDATA[video]]></category><category><![CDATA[AR]]></category><category><![CDATA[React]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Tushar Tripathi]]></dc:creator><pubDate>Tue, 22 Feb 2022 16:14:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1635679144298/ATme9cjey.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>How cool would it be if you could build your own Video Call app with Snapchat-like filters in it!
<img src="https://c.tenor.com/QctWuksBNPQAAAAC/super-cool-and-amazing-corey-vidal.gif" alt="Super Cool" />
Ikr! That's what I was thinking when I came across <a target="_blank" href="https://github.com/jeeliz/jeelizFaceFilter">Jeeliz</a>. Now I have worked with <code>tensorflow.js</code> based libraries in the past but they're usually quite CPU intensive for a live video use case. Jeeliz looked promising as it's designed for this use case. So I thought why not try it out by adding some 3d AR filters to our video calls. Well! that is what we're going to do.</p>
<p>We'll use React and 100ms' React SDK for the video call part of our application. <a target="_blank" href="https://100ms.live/">100ms</a>, in short, is building developer-focused live SDKs which abstracts away the low-level complexities. Support for <a target="_blank" href="https://docs.100ms.live/javascript/v2/plugins/custom-video-plugins">video plugins</a> was released recently which makes it easier to experiment with AR filters after setting up a basic app. And so I set forth on the journey. I'll mostly be talking about the implementation details related to the filters themselves in this blog than setting up the video call app from scratch. You can checkout the <a target="_blank" href="https://docs.100ms.live/javascript/v2/guides/react-quickstart">quickstart guide</a> though for a quick overview of the SDK and how it works, or you can just fork it(it's also the first step 😀) and follow along with my exploration.</p>
<p>I have split the blog into parts so it's not overwhelming. In this part, we'll try to understand the plugin interface exposed by the SDK, learn a bit about HTML Canvas elements and implement a basic filter. We'll go into more details about AR, WebGL, and implementing the AR filter plugin in further parts. </p>
<p>Everything we'll do is available in <a target="_blank" href="https://github.com/triptu/100ms-face-filters">this Github repo</a>, and I have linked to the relevant commit for each step. By the end of this blog, we'll be able to build a simple grayscale filter -</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635601059289/TXwyJDmXc.gif" alt="grayscale.gif" />
Looks cool? You can check the demo of the final thing <a target="_blank" href="https://hms-face-filters.netlify.app/">here</a>. Let's get started with the code part.</p>
<h3 id="heading-fork-the-quickstart">Fork the quickstart</h3>
<p>This step can be skipped if you're integrating filters in an existing web app already using the 100ms SDKs. If that is not the case let's start with forking the <a target="_blank" href="https://codesandbox.io/s/happy-meddling-syndrome-q4ukf">codesandbox</a> linked in the doc to a GitHub repo. Now I have already done it so forking my GitHub <a target="_blank" href="https://github.com/triptu/100ms-face-filters/tree/original">repo</a> will be much faster. The initial code lies in the branch named <code>original</code>.</p>
<p>You can also checkout the branch to follow locally -</p>
<pre><code class="lang-sh">git <span class="hljs-built_in">clone</span> -b original https://github.com/triptu/100ms-face-filters.git
</code></pre>
<h3 id="heading-run-the-app-locally">Run the app locally</h3>
<p>We can clone the repo now and run it locally. Feel free to update to the latest versions <a target="_blank" href="https://docs.100ms.live/javascript/v2/release-notes/release-notes">here</a> of the SDKs and then run using <code>yarn install</code> followed by <code>yarn start</code>. We'll see a screen like this if everything worked fine - </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635600953391/0x85ze9hD.png" alt="Screenshot 2021-10-30 at 7.05.38 PM.png" /></p>
<p>In case you're wondering what that auth token is, we can imagine them to be the meeting id that tells 100ms which room to put you in. Getting such a token is fairly straightforward(doesn't require anything technical or code) and is given in more detail <a target="_blank" href="https://docs.100ms.live/javascript/v2/guides/token">here</a>. Once you get the token, verify that everything is working fine. You can try joining from multiple tabs or sharing the link with your friends(after exposing with <a target="_blank" href="https://ngrok.com/">ngrok</a> ofc). You can also join the same room from the link available on the dashboard(where the token was copied from).</p>
<h3 id="heading-grayscale-filter">Grayscale Filter</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635604908662/56U3xd7jq.png" alt="colorpixels.png" />
Let's say that we have to convert a colorful image to Grayscale and we're wondering what would it take. To answer this let's try to break down the image into further parts. An image is a matrix of many pixels where a single pixel can be described using three numbers from 0-255, the intensity values of red, green and blue. For a grayscale image, each pixel can be described as only one number ranging from 0-255 with 0 being black(lowest intensity) and 255 being white(highest intensity).
Now if we were to convert a colored pixel with RGB values into grayscale, we will need some sort of mapping between both. A fairly straightforward way to map these is to average out the three intensities -</p>
<pre><code class="lang-js">intensity = (red + blue + green)/<span class="hljs-number">3</span>
</code></pre>
<p>But this won't result in a balanced grayscale image. The reason for it is that our eyes react differently to each color being most sensitive to green and least to blue. For our filter, we'll go with <a target="_blank" href="https://en.wikipedia.org/wiki/Luma_(video">Luma</a> which is a weighted sum of the RGB values and maps to the luminance much more accurately.</p>
<pre><code class="lang-js"><span class="hljs-comment">// Luma</span>
intensity = red * <span class="hljs-number">0.299</span> + green * <span class="hljs-number">0.587</span> + blue * <span class="hljs-number">0.114</span>
</code></pre>
<h3 id="heading-going-through-the-plugin-docs">Going through the Plugin Docs</h3>
<p>Now that we're all set with the algorithm to convert an RGB image to grayscale, let's move ahead with checking out how we can write a plugin to implement this. The documentation is <a target="_blank" href="https://docs.100ms.live/javascript/v2/plugins/custom-video-plugins">here</a>, and fortunately, I've read it so you don't have to. </p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635607283807/BVOvsDJtV.gif" alt="plugindocs.gif" /></p>
<p>The gist of it is that we have to write a class that implements a method <code>processVideoFrame(inputCanvas, outputCanvas)</code>, where we're passed in an image on the input canvas and have to put a result image on the output canvas. This makes the job fairly easy for us as we don't have to worry about video but just one image at a time. So as long as we can find a way to get RGB values from the input canvas and put the grayscale values on the output canvas, we should be able to implement the algorithm discussed and we'll be good.</p>
<h3 id="heading-implementing-the-grayscale-plugin">Implementing the Grayscale Plugin</h3>
<p>Checkout the full commit <a target="_blank" href="https://github.com/triptu/100ms-face-filters/commit/be082a87e1330d66d18eb32e702881558d26e183">here</a>.</p>
<p>So as we figured out from the docs, it's <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API">HTML Canvas</a> we're going to deal with. Now canvas has something called a context which exposes direct methods both for getting the RGB values from a canvas(<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getImageData">getImageData</a>) and applying them(<a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/putImageData">putImageData</a>). With this information, we can begin writing our GrayScale Plugin. I have added further comments in the code below. Note that some other methods are present too as they're required by the SDK.</p>
<pre><code class="lang-js"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GrayscalePlugin</span> </span>{
   <span class="hljs-comment">/**
   * <span class="hljs-doctag">@param </span>input {HTMLCanvasElement}
   * <span class="hljs-doctag">@param </span>output {HTMLCanvasElement}
   */</span>
  processVideoFrame(input, output) {
    <span class="hljs-comment">// we don't want to change the dimensions so set the same width, height</span>
    <span class="hljs-keyword">const</span> width = input.width;
    <span class="hljs-keyword">const</span> height = input.height;
    output.width = width;
    output.height = height;
    <span class="hljs-keyword">const</span> inputCtx = input.getContext(<span class="hljs-string">"2d"</span>);
    <span class="hljs-keyword">const</span> outputCtx = output.getContext(<span class="hljs-string">"2d"</span>);
    <span class="hljs-keyword">const</span> imgData = inputCtx.getImageData(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, width, height);
    <span class="hljs-keyword">const</span> pixels = imgData.data; 
    <span class="hljs-comment">// pixels is an array of all the pixels with their RGBA values, the A stands for alpha</span>
    <span class="hljs-comment">// we will not actually be using alpha for this plugin, but we still need to skip it(hence the i+= 4)</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; pixels.length; i += <span class="hljs-number">4</span>) {
      <span class="hljs-keyword">const</span> red = pixels[i];
      <span class="hljs-keyword">const</span> green = pixels[i + <span class="hljs-number">1</span>];
      <span class="hljs-keyword">const</span> blue = pixels[i + <span class="hljs-number">2</span>];
      <span class="hljs-comment">// the luma algorithm as we discussed above, floor because intensity is a number</span>
      <span class="hljs-keyword">const</span> lightness = <span class="hljs-built_in">Math</span>.floor(red * <span class="hljs-number">0.299</span> + green * <span class="hljs-number">0.587</span> + blue * <span class="hljs-number">0.114</span>);
      <span class="hljs-comment">// all of RGB is set to the calculated intensity value for grayscale</span>
      pixels[i] = pixels[i + <span class="hljs-number">1</span>] = pixels[i + <span class="hljs-number">2</span>] = lightness;
    }
    <span class="hljs-comment">// and finally now that we have the updated values for grayscale we put it on output</span>
    outputCtx.putImageData(imgData, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>);
  }

  getName() {
    <span class="hljs-keyword">return</span> <span class="hljs-string">"grayscale-plugin"</span>;
  }

  isSupported() {
    <span class="hljs-comment">// we're not doing anything complicated, it's supported on all browsers</span>
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
  }

  <span class="hljs-keyword">async</span> init() {} <span class="hljs-comment">// placeholder, nothing to init</span>

  getPluginType() {
    <span class="hljs-keyword">return</span> HMSVideoPluginType.TRANSFORM; <span class="hljs-comment">// because we transform the image</span>
  }

  stop() {} <span class="hljs-comment">// placeholder, nothing to stop</span>
}
</code></pre>
<h3 id="heading-adding-a-button-component-to-add-the-plugin">Adding a button component to add the plugin</h3>
<p>Checkout the full commit <a target="_blank" href="https://github.com/triptu/100ms-face-filters/commit/934137bf423a9b4a2256279a2d1b9981167a9ffd">here</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635612600249/7YliY5Gdk.png" alt="buttonadded.png" /></p>
<p>Let's write a toggle button component now which will turn on/off the filter. The component will take in a plugin and button name to display. </p>
<pre><code class="lang-jsx"><span class="hljs-comment">// also intialise the grayscale plugin for use by the Button's caller</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> grayScalePlugin = <span class="hljs-keyword">new</span> GrayscalePlugin();

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PluginButton</span>(<span class="hljs-params">{ plugin, name }</span>) </span>{
  <span class="hljs-keyword">const</span> isPluginAdded = <span class="hljs-literal">false</span>;
  <span class="hljs-keyword">const</span> togglePluginState = <span class="hljs-keyword">async</span> () =&gt; {};

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"btn"</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{togglePluginState}</span>&gt;</span>
      {`${isPluginAdded ? "Remove" : "Add"} ${name}`}
    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
  );
}
</code></pre>
<p>We'll use it as below, this is added in the header component in the above commit.</p>
<pre><code class="lang-jsx">&lt;PluginButton plugin={grayScalePlugin} name={<span class="hljs-string">"Grayscale"</span>} /&gt;
</code></pre>
<p>Clicking on the button won't work yet though, because we're not adding the plugin to the video track. Let's see how to do that in the next section.</p>
<h3 id="heading-making-the-button-functional">Making the button functional</h3>
<p>Checkout the full commit <a target="_blank" href="https://github.com/triptu/100ms-face-filters/commit/9f129591907847283185ae1c29d093557da14b90">here</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1635611462076/B1mft3uOV.gif" alt="working.gif" /></p>
<p>With some help from the <a target="_blank" href="https://docs.100ms.live/javascript/v2/plugins/custom-video-plugins#adding-and-removing-plugins">documentation</a>, we can make our button component functional using the hooks exposed by the SDK. There are two hooks from the SDK we need to use to implement our toggle function -</p>
<ol>
<li><code>useHMSStore</code> for knowing the current state i.e. whether the plugin is currently part of the video track.</li>
<li><code>useHMSActions</code> to get access to the methods for adding and removing the plugin.</li>
</ol>
<pre><code class="lang-jsx"><span class="hljs-keyword">import</span> {
  selectIsLocalVideoPluginPresent,
  useHMSActions,
  useHMSStore,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@100mslive/react-sdk"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PluginButton</span>(<span class="hljs-params">{ plugin, name }</span>) </span>{
  <span class="hljs-keyword">const</span> isPluginAdded = useHMSStore(
    selectIsLocalVideoPluginPresent(plugin.getName())
  );
  <span class="hljs-keyword">const</span> hmsActions = useHMSActions();

  <span class="hljs-keyword">const</span> togglePluginState = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!isPluginAdded) {
      <span class="hljs-keyword">await</span> hmsActions.addPluginToVideoTrack(plugin);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-keyword">await</span> hmsActions.removePluginFromVideoTrack(plugin);
    }
  };

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"btn"</span> <span class="hljs-attr">onClick</span>=<span class="hljs-string">{togglePluginState}</span>&gt;</span>
      {`${isPluginAdded ? "Remove" : "Add"} ${name}`}
    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
  );
}
</code></pre>
<h3 id="heading-voila">Voilà!</h3>
<p>And that's it, our button is functional now. Everything works and looks amazing. To recap, we were able to write a grayscale filter from scratch which transforms our video for everyone in the room. </p>
<p><img src="https://c.tenor.com/ZlbaQ4CijJMAAAAC/theo-theo-lawrence.gif" alt="We did it" /></p>
<p>You can go on from here to have more filters(for e.g. <a target="_blank" href="https://stackoverflow.com/questions/1061093/how-is-a-sepia-tone-created">sepia</a>, saturation, contrast), or experiment with other image processing algorithms to explore the possibilities. Check out <a target="_blank" href="https://ai.stanford.edu/~syyeung/cvweb/tutorial1.html">this</a> and <a target="_blank" href="https://towardsdatascience.com/image-processing-and-pixel-manipulation-photo-filters-5d37a2f992fa">this</a> for some starting points.We'll talk about creating an AR filter in upcoming parts which will build upon the fundamentals learned in this part.</p>
]]></content:encoded></item></channel></rss>