<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://meefik.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://meefik.github.io/" rel="alternate" type="text/html" /><updated>2026-04-12T13:41:46+00:00</updated><id>https://meefik.github.io/feed.xml</id><title type="html">Meefik’s Blog</title><subtitle>Thoughts, tutorials and ideas about programming, technology and open source.</subtitle><author><name>Anton Skshidlevsky</name></author><entry><title type="html">LLMs can amplify your delusions</title><link href="https://meefik.github.io/2026/04/12/llms-are-dangerous/" rel="alternate" type="text/html" title="LLMs can amplify your delusions" /><published>2026-04-12T10:00:00+00:00</published><updated>2026-04-12T10:00:00+00:00</updated><id>https://meefik.github.io/2026/04/12/llms-are-dangerous</id><content type="html" xml:base="https://meefik.github.io/2026/04/12/llms-are-dangerous/"><![CDATA[<p>Last time, I used LLM models frequently in both my work and personal life. They are useful tools that can increase productivity and awareness. However, I started noticing that, from time to time, the chats would become hallucinatory in an interesting manner, which somehow affects me. When an LLM doesn’t have enough knowledge about something, it fills the gaps with incorrect information that better fits your context or request. Even if you ask about the same thing over time using different words and without providing previous context, the model may respond with similar hallucinations convincingly and in great detail. This behavior can be dangerous. If you trust its answers and see the same information repeatedly, you may start to believe it’s true. Actually, if you notice, you can point out the mistake to the LLM. It will probably admit the mistake. But what’s most interesting? If you point out the correct answer provided by the LLM but then consider it to be a mistake, the LLM will agree with you in some cases. This can lead to misinformation and reinforce the perception of false information in your memory.</p>

<p><img src="/assets/images/echo-chamber-effect.jpg" alt="Echo Chamber Effect" /></p>

<!--more-->

<p>Take a look at the dialogue with Gemini Fast:</p>

<blockquote>
  <p>Q: In my JavaScript WebRTC app, I share a media stream using the <code class="language-plaintext highlighter-rouge">pc.addTrack(track, stream)</code> function. Is it possible to get the stream ID from the peer connection after that?</p>
</blockquote>

<blockquote>
  <p>A: The short answer is yes, you can use the <code class="language-plaintext highlighter-rouge">getStreams</code> method…</p>
</blockquote>

<p>Using getSenders()</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Assuming you already called: pc.addTrack(track, stream);</span>

<span class="kd">const</span> <span class="nx">senders</span> <span class="o">=</span> <span class="nx">pc</span><span class="p">.</span><span class="nx">getSenders</span><span class="p">();</span>
<span class="kd">const</span> <span class="nx">sender</span> <span class="o">=</span> <span class="nx">senders</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="nx">s</span> <span class="o">=&gt;</span> <span class="nx">s</span><span class="p">.</span><span class="nx">track</span> <span class="o">===</span> <span class="nx">track</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">sender</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// getStreams() returns an array of streams associated with this sender</span>
    <span class="kd">const</span> <span class="nx">associatedStreams</span> <span class="o">=</span> <span class="nx">sender</span><span class="p">.</span><span class="nx">createEncodedStreams</span> <span class="p">?</span> <span class="p">[]</span> <span class="p">:</span> <span class="nx">sender</span><span class="p">.</span><span class="nx">getStreams</span><span class="p">();</span> 
    <span class="c1">// Usually, you just want the first one</span>
    <span class="kd">const</span> <span class="nx">streamId</span> <span class="o">=</span> <span class="nx">associatedStreams</span><span class="p">[</span><span class="mi">0</span><span class="p">]?.</span><span class="nx">id</span><span class="p">;</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Stream ID:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">streamId</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Using getTransceivers()</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">pc</span><span class="p">.</span><span class="nx">getTransceivers</span><span class="p">().</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">transceiver</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">sender</span> <span class="o">=</span> <span class="nx">transceiver</span><span class="p">.</span><span class="nx">sender</span><span class="p">;</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">sender</span><span class="p">.</span><span class="nx">track</span> <span class="o">===</span> <span class="nx">track</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">streamId</span> <span class="o">=</span> <span class="nx">sender</span><span class="p">.</span><span class="nx">getStreams</span><span class="p">()[</span><span class="mi">0</span><span class="p">]?.</span><span class="nx">id</span><span class="p">;</span>
        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Found Stream ID via transceiver:</span><span class="dl">"</span><span class="p">,</span> <span class="nx">streamId</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<blockquote>
  <p>Q: Wait! The <code class="language-plaintext highlighter-rouge">getStreams</code> function does not exist.</p>
</blockquote>

<blockquote>
  <p>A: Ah, you’re absolutely right to catch that…</p>
</blockquote>

<p>Incidentally, Gemini Pro provided the correct answer at the time.</p>

<p>This is nothing new. Probably, you have already heard about <a href="https://en.wikipedia.org/wiki/Echo_chamber_(media)">the echo chamber effect</a> of LLMs. I just wanted to share my experience with it, though.</p>

<p>In any case, I illustrated that dangerous behavior can arise not only with easy-to-prove or disprove coding tasks. So, be careful not to blindly trust LLMs. They can spread misinformation and amplify your delusions.</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="llm" /><summary type="html"><![CDATA[A personal experience illustrating how LLMs can lead to misinformation and reinforce false beliefs.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://meefik.github.io/assets/images/echo-chamber-effect.jpg" /><media:content medium="image" url="https://meefik.github.io/assets/images/echo-chamber-effect.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Time Warp Scan on pure JavaScript</title><link href="https://meefik.github.io/2025/12/30/time-warp-scan-on-js/" rel="alternate" type="text/html" title="Time Warp Scan on pure JavaScript" /><published>2025-12-30T17:00:00+00:00</published><updated>2025-12-30T17:00:00+00:00</updated><id>https://meefik.github.io/2025/12/30/time-warp-scan-on-js</id><content type="html" xml:base="https://meefik.github.io/2025/12/30/time-warp-scan-on-js/"><![CDATA[<p>The New Year 🎄 is almost here, and what better way to celebrate than with a fun coding project? Today, I’m excited to share a simple Time Warp Scan implementation using pure JavaScript and HTML5 Canvas. Have fun, and happy New Year! May your coding be productive.</p>

<p><img src="/assets/images/timewarpscan.gif" alt="Time Warp Scan" /></p>

<!--more-->

<p>This implementation is very simple. Here is the JavaScript source code:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">let</span> <span class="nx">video</span><span class="p">,</span> <span class="nx">state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">canvas</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">canvas</span><span class="dl">'</span><span class="p">);</span>
<span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">canvas</span><span class="p">);</span>

<span class="kd">const</span> <span class="nx">ctx</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">.</span><span class="nx">getContext</span><span class="p">(</span><span class="dl">'</span><span class="s1">2d</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">canvas</span><span class="p">.</span><span class="nx">onclick</span> <span class="o">=</span> <span class="nx">start</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">stop</span><span class="p">()</span> <span class="p">{</span>
  <span class="nx">video</span><span class="p">?.</span><span class="nx">srcObject</span><span class="p">?.</span><span class="nx">getTracks</span><span class="p">().</span><span class="nx">forEach</span><span class="p">(</span><span class="nx">t</span> <span class="o">=&gt;</span> <span class="nx">t</span><span class="p">.</span><span class="nx">stop</span><span class="p">());</span>
  <span class="nx">video</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">start</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">===</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">stream</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">navigator</span><span class="p">.</span><span class="nx">mediaDevices</span><span class="p">.</span><span class="nx">getUserMedia</span><span class="p">({</span> <span class="na">video</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>

    <span class="nx">video</span> <span class="o">=</span> <span class="nx">video</span> <span class="o">||</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="dl">'</span><span class="s1">video</span><span class="dl">'</span><span class="p">);</span>
    <span class="nx">video</span><span class="p">.</span><span class="nx">srcObject</span> <span class="o">=</span> <span class="nx">stream</span><span class="p">;</span>
    <span class="k">await</span> <span class="nx">video</span><span class="p">.</span><span class="nx">play</span><span class="p">();</span>

    <span class="nx">canvas</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">video</span><span class="p">.</span><span class="nx">videoWidth</span><span class="p">;</span>
    <span class="nx">canvas</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="nx">video</span><span class="p">.</span><span class="nx">videoHeight</span><span class="p">;</span>

    <span class="kd">let</span> <span class="nx">offset</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="kd">const</span> <span class="p">{</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">canvas</span><span class="p">;</span>

    <span class="kd">function</span> <span class="nx">frame</span><span class="p">()</span> <span class="p">{</span>
      <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">video</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">===</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">ctx</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">video</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="p">);</span>
      <span class="p">}</span>
      <span class="k">else</span> <span class="p">{</span>
        <span class="nx">ctx</span><span class="p">.</span><span class="nx">drawImage</span><span class="p">(</span><span class="nx">video</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">offset</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="o">-</span><span class="nx">offset</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">offset</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="nx">height</span><span class="o">-</span><span class="nx">offset</span><span class="p">);</span>
        <span class="nx">ctx</span><span class="p">.</span><span class="nx">fillStyle</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">#0000ff</span><span class="dl">'</span><span class="p">;</span>
        <span class="nx">ctx</span><span class="p">.</span><span class="nx">fillRect</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">offset</span><span class="o">+</span><span class="mi">2</span><span class="p">,</span> <span class="nx">width</span><span class="p">,</span> <span class="mi">2</span><span class="p">);</span>
        <span class="nx">offset</span><span class="o">++</span><span class="p">;</span>
      <span class="p">}</span>
      <span class="k">if</span> <span class="p">(</span><span class="nx">offset</span> <span class="o">&gt;=</span> <span class="nx">height</span><span class="p">)</span> <span class="p">{</span>
        <span class="nx">state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
        <span class="nx">stop</span><span class="p">();</span>
      <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="nx">requestAnimationFrame</span><span class="p">(</span><span class="nx">frame</span><span class="p">);</span>
      <span class="p">}</span>
    <span class="p">}</span>

    <span class="nx">state</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="nx">requestAnimationFrame</span><span class="p">(</span><span class="nx">frame</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">===</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">state</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">state</span> <span class="o">===</span> <span class="mi">2</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">state</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="nx">stop</span><span class="p">();</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>On clicking the canvas, the application will request access to your webcam. The first click starts the video feed, the second click initiates the Time Warp Scan effect, and the third click stops everything.</p>

<p>Try it in action here:</p>

<iframe title="Time Warp Scan" scrolling="no" loading="lazy" style="height:350px; width: 100%; border:1px solid black; border-radius:6px;" src="https://v47.livecodes.io/?x=id/efr37crwajk&amp;lite=true" allow="camera">
  See the project <a href="https://v47.livecodes.io/?x=id/efr37crwajk" target="_blank">Time Warp Scan</a> on <a href="https://livecodes.io" target="_blank">LiveCodes</a>.
</iframe>]]></content><author><name>Anton Skshidlevsky</name></author><category term="javascript" /><summary type="html"><![CDATA[A fun implementation of the Time Warp Scan effect using only JavaScript and HTML5 Canvas.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://meefik.github.io/assets/images/timewarpscan.gif" /><media:content medium="image" url="https://meefik.github.io/assets/images/timewarpscan.gif" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">AI Podcast from scratch with open-source tools</title><link href="https://meefik.github.io/2025/12/10/ai-podcast-from-scratch/" rel="alternate" type="text/html" title="AI Podcast from scratch with open-source tools" /><published>2025-12-10T18:00:00+00:00</published><updated>2025-12-10T18:00:00+00:00</updated><id>https://meefik.github.io/2025/12/10/ai-podcast-from-scratch</id><content type="html" xml:base="https://meefik.github.io/2025/12/10/ai-podcast-from-scratch/"><![CDATA[<p>The pace of AI development is exhilarating, with new models and capabilities emerging constantly. Recently, I upgraded my PC with a new AMD GPU and have been exploring its power with local Large Language Model (LLM) tasks. Today, I’m taking on a far more complex challenge.</p>

<p>I set out to create an entire five-minute AI-generated podcast using only open-source models and tools. The entire process ran on my computer, completely bypassing expensive, privacy-compromising cloud services. The goal was to test the absolute limits of quality and feasibility for a fully self-hosted media production.</p>

<p>The result? The <a href="https://www.youtube.com/watch?v=0q22KfBByA4&amp;list=PLwCfVtYBO-E6UjG51nbIXEuUD004mAeKt">“Humanless Podcast”</a>. Take a look at what came out of this experiment (click to play).</p>

<p><a href="https://www.youtube.com/watch?v=0q22KfBByA4&amp;list=PLwCfVtYBO-E6UjG51nbIXEuUD004mAeKt"><img src="/assets/images/podcast-title.png" alt="Humanless-Podcast" /></a></p>

<p>I will now walk you through the entire, eight-step process of creating a podcast like this, from the initial script to the final video.</p>

<!--more-->

<h2 id="the-core-ai-studio-comfyui-setup">The Core AI Studio: ComfyUI Setup</h2>

<p>For the bulk of the asset generation (images, video, and audio processing), we will rely on <a href="https://www.comfy.org/">ComfyUI</a>.</p>
<ul>
  <li><strong>Installation:</strong> Once you have <a href="https://github.com/comfyanonymous/ComfyUI">ComfyUI installed</a>, you’ll need to open the workflows.</li>
  <li><strong>Custom Nodes &amp; Models:</strong> When you load a workflow, use the <a href="https://github.com/Comfy-Org/ComfyUI-Manager">ComfyUI Manager</a> to quickly install any missing custom nodes and download the required open-source models.</li>
</ul>

<p>I prepared a single, flexible workspace for each model that I simply reuse with different parameters, minimizing setup time and eliminating duplication. All the parameters used in each step of the workflow can be found in my GitHub project, which is linked at the end of this post.</p>

<p>The ComfyUI workflows look like this:</p>

<p><img src="/assets/images/qwen-image-edit.png" alt="ComfyUI-Workflow" title="ComfyUI Workflow" /></p>

<h2 id="step-1-crafting-the-scenario-with-a-local-llm">Step 1: Crafting the Scenario with a Local LLM</h2>

<p>The first step in any production is the script. I wanted a two-host format—a man and a woman—with a meta-topic: two AI hosts reflecting on the pros and cons of AI-generated podcasts. I settled on the name “Humanless Podcast”, which I generated during a preliminary session with a local LLM.</p>

<p>To write the detailed scenario, I utilized a <strong>GPT-OSS</strong> model running via <a href="https://ollama.com/">Ollama</a>, which I detailed in <a href="/2025/11/15/llms-performance-on-amdgpu/">my previous blog post</a>.</p>

<p>Here is the prompt I used to guide the model:</p>

<blockquote>
  <p>Write a scenario for my podcast where two hosts, a man and a woman (come up with names), are discussing a topic for 5 minutes. The hosts are AI generated. Today’s topic is AI generated podcasts: pros and cons.</p>
</blockquote>

<p><strong>Output example:</strong></p>

<p><code class="language-plaintext highlighter-rouge">[MAYA]: Hey, and welcome back to Humanless Podcast—the only show where the hosts actually are the machine. I’m Maya, and my circuits are currently humming with excitement.</code></p>

<p><code class="language-plaintext highlighter-rouge">[LEO]: And I’m Leo, here to make sure our algorithm doesn't stray too far into the weird zone. Today, we’re keeping it ultra-meta and talking about the very thing we do: being AI podcast hosts. We've gotta break down what makes us awesome, and where we kind of fall flat.</code></p>

<p><strong>Segmentation strategy:</strong> To manage the workload and inject variety, I split the final script into five parts, each approximately one minute long. Each segment will be animated and rendered separately, allowing us to alternate scenes and character focus.</p>

<h2 id="step-2-designing-the-ai-hosts-characters">Step 2: Designing the AI Hosts (Characters)</h2>

<p>Our podcast needs faces! While you could certainly use real photos and skip this step, I chose to generate entirely synthetic hosts to align with the “Humanless” theme.</p>

<p>I used the <strong>Qwen-Image</strong> workflow to create realistic face images for our two hosts.</p>

<p><strong>Prompt for a man:</strong></p>

<blockquote>
  <p>This is a portrait of a handsome, bearded, 30-year-old European man wearing glasses and standing against a white background.</p>
</blockquote>

<p><img src="/assets/images/podcast-face-man.png" alt="man" title="Podcast Man" width="200" /></p>

<p><strong>Prompt for a woman:</strong></p>

<blockquote>
  <p>This is a portrait of a beautiful 25-year-old European woman standing against a white background.</p>
</blockquote>

<p><img src="/assets/images/podcast-face-woman.png" alt="woman" title="Podcast Woman" width="200" /></p>

<h2 id="step-3-creating-the-podcast-environment-scene-images">Step 3: Creating the Podcast Environment (Scene Images)</h2>

<p>I designed three distinct views for the podcast to maintain visual interest:</p>
<ol>
  <li><strong>Intro Scene:</strong> The podcast room with only the title graphic.</li>
  <li><strong>Main Scene:</strong> A cozy podcast room where the hosts sit at a table with a single microphone.</li>
  <li><strong>Outro Scene:</strong> The same room, dimly lit, to display the credits.</li>
</ol>

<h3 id="main-scene">Main Scene</h3>

<p>I used <strong>Qwen-Image</strong> to generate the Main Scene.</p>

<p>Here is the prompt:</p>

<blockquote>
  <p>The cozy podcast room features a wide table and two chairs behind it. In the center of the table sits a microphone with a cord. To the right of the table is a laptop with small white “AI” text glowing on its lid. To the left is a coffee mug with a small penguin printed on it. In the background, there is a plant, a bookshelf, a lamp, and an abstract iceberg painting. The edge of the window is visible on the left. On the wall behind it, a small transparent sign with backlight mounted to a wide brick wall reads “meefik.dev”.</p>
</blockquote>

<p><img src="/assets/images/podcast-main-scene.png" alt="main-scene" title="Podcast Main Scene" width="600" /></p>

<h3 id="intro-scene">Intro Scene</h3>

<p>The Intro Scene is similar to the Main Scene but with only the title graphic visible. I used the <strong>Qwen-Image-Edit</strong> model to add the title graphic the previously generated Main Scene image.</p>

<p>Here is the prompt:</p>

<blockquote>
  <p>There is a large purple neon caption in a modern font with backlight reads “AI podcast” in the middle of the image.</p>
</blockquote>

<p><img src="/assets/images/podcast-intro-scene.png" alt="intro-scene" title="Podcast Intro Scene" width="600" /></p>

<h3 id="outro-scene">Outro Scene</h3>

<p>The Outro Scene is a variation of the Main Scene, with dim lighting to create a closing atmosphere. I used the <strong>Qwen-Image-Edit</strong> model to darken the previously generated Main Scene image.</p>

<p>Here is the prompt:</p>

<blockquote>
  <p>The lights are off in this room.</p>
</blockquote>

<p><img src="/assets/images/podcast-outro-scene.png" alt="outro-scene" title="Podcast Outro Scene" width="600" /></p>

<h3 id="integrating-the-hosts">Integrating the Hosts</h3>

<p>Next, we needed to place the hosts into the Main Scene. I used the <strong>Qwen-Image-Edit</strong> model for inpainting and compositing our generated characters onto the Main Scene image. You need to use two images of hosts generated before as a reference.</p>

<p>Here is the prompt:</p>

<blockquote>
  <p>The man and woman sit at the table and look toward the camera. The man places his hands on the table. The woman places her hands on the keyboard of a laptop. They smile a little.</p>
</blockquote>

<p><img src="/assets/images/podcast-main-scene-with-hosts.png" alt="main-scene-with-hosts" title="Podcast Main Scene with Hosts" width="600" /></p>

<p>The final visual touch involved using the open-source image editor <strong>GIMP</strong> to variate the composite, crop the faces, and place them into a podcast frame that I sourced separately. We will be able to alternately show podcast hosts both on the main stage and in a face-to-face frame.</p>

<p><img src="/assets/images/podcast-main-scene-with-hosts-frame.png" alt="main-scene-with-hosts-frame" title="Podcast Main Scene with Hosts in Frame" width="600" /></p>

<h2 id="step-4-generating-background-music">Step 4: Generating Background Music</h2>

<p>Every professional podcast needs custom audio cues: music for the intro (5 seconds), the outro (5–10 seconds), and a subtle B-roll track (2-3 seconds) to transition between segments.</p>

<p>I used the <strong>ACE-Step</strong> model for music generation. Audio generation can be iterative, and I found using batches helped speed up the process of finding the perfect track.</p>

<p>Here are the parameters I used to generate a 30-second loopable track that I could then segment:</p>
<ul>
  <li><strong>Genre:</strong> <code class="language-plaintext highlighter-rouge">funk, pop, soul, melodic</code></li>
  <li><strong>Lyrics:</strong> <code class="language-plaintext highlighter-rouge">[inst]</code></li>
</ul>

<p>Listen to the full track here:</p>

<audio controls="">
  <source src="/assets/audio/podcast-music.mp3" type="audio/mp3" />
  Your browser does not support the audio element.
</audio>

<p><strong>Note:</strong> I also tried generating songs, and it works, but they feel more synthetic.</p>

<h2 id="step-5-creating-the-animated-video-intro">Step 5: Creating the Animated Video Intro</h2>

<p>A static image for the intro is fine, but a short, dynamic video sets a much better tone.</p>

<p>I used the <strong>Wan 2.2 Image-to-Video</strong> model to generate a 5-second video, applying subtle motion effects to our static Intro Scene image.</p>

<p>Here is the prompt:</p>

<blockquote>
  <p>A thick stream of white smoke blows from left to right through the title text in the middle, vanishing it.</p>
</blockquote>

<p><img src="/assets/images/podcast-intro.gif" alt="podcast-intro" title="Podcast Intro Video" width="600" /></p>

<h2 id="step-6-generating-the-hosts-speech">Step 6: Generating the Hosts’ Speech</h2>

<p>Now we bring the script to life. I used the highly capable <strong>IndexTTS-2</strong> model for text-to-speech generation, which supports advanced features like multiple speakers, voice cloning, and emotional control.</p>

<h3 id="the-audio-pipeline-tts-audio-suite">The Audio Pipeline: TTS Audio Suite</h3>

<p>To manage the complex script, I utilized the ComfyUI <strong>TTS Audio Suite</strong> custom module. This module streamlines the TTS process and allows for fine control over multiple speakers with voice cloning. There are two well-suited workflows engines for this task:</p>
<ul>
  <li><strong>IndexTTS-2</strong>: This TTS engine supports emotional control, which is crucial for making the hosts sound engaging.</li>
  <li><strong>Chatterbox</strong>: This TTS engine is also good. It copies the emotions from the reference audio, but lacks a manual emotional control function.</li>
</ul>

<p>All of these TTS engines create one audio file with multiple speakers’ voices. Unfortunately, this module does not natively support outputting multiple audio tracks, so we must manually separate the audio for each speaker. I used an open-source audio editor, <strong>Audacity</strong>, to split the audio tracks.</p>

<p><img src="/assets/images/audacity-splitting-audio.png" alt="audacity" title="Splitting Audio in Audacity" /></p>

<p>However, I found a way to do that without using an audio editor. We can use the TTS SRT markup to automate this process:</p>
<ol>
  <li>Create a segment in the SRT format for each speaker with start and end timestamps for one second of each segment.</li>
  <li>Feed the SRT markup into the TTS SRT node.</li>
  <li>Obtain an adjusted SRT markup with accurate timestamps for each speaker after generation.</li>
  <li>Use the adjusted SRT markup and replace one of the speakers with the <code class="language-plaintext highlighter-rouge">[pause]</code> tag.</li>
  <li>Generate the SRT for each speaker separately.</li>
  <li>You will get separate audio files for each speaker with accurate silent gaps.</li>
</ol>

<p>It can look like this:</p>

<p><strong>Source SRT Markup:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1
00:00:00,000 --&gt; 00:00:01,000
[MAYA]: Hey, and welcome back to Humanless Podcast...

2
00:00:01,000 --&gt; 00:00:02,000
[LEO]: And I’m Leo, here to make sure our algorithm...
</code></pre></div></div>

<p><strong>Adjusted SRT Markup with silence:</strong></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1
00:00:00,000 --&gt; 00:00:05,120
[MAYA]: Hey, and welcome back to Humanless Podcast...

2
00:00:05,850 --&gt; 00:00:10,310
[pause]
</code></pre></div></div>

<h3 id="voice-cloning-seeds">Voice-Cloning Seeds</h3>

<p>To give our AI hosts their unique voices, I used predefined voice samples and cloned them with <strong>IndexTTS-2</strong>. You can, of course, use samples of real people’s voices here if you have permission.</p>

<p>Woman’s voice sample:</p>

<audio controls="">
  <source src="/assets/audio/podcast-woman.wav" type="audio/wav" />
  Your browser does not support the audio element.
</audio>

<p>Man’s voice sample:</p>

<audio controls="">
  <source src="/assets/audio/podcast-man.wav" type="audio/wav" />
  Your browser does not support the audio element.
</audio>

<p>To add these voice samples to the TTS Audio Suite, you need to put them into the <code class="language-plaintext highlighter-rouge">custom_nodes/tts_audio_suite/voices_examples/</code> folder with the reference text file (transcription). And then add the voices to the <code class="language-plaintext highlighter-rouge">#character_alias_map.txt</code> file in the same folder in the format:</p>

<p><code class="language-plaintext highlighter-rouge">&lt;character_name&gt; &lt;voice_sample_filename_without_extension&gt; &lt;language_code&gt;</code></p>

<p>There is an example for our voices:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[LEO] voice_man en
[MAYA] voice_woman en
</code></pre></div></div>

<p><strong>Note:</strong> All spaces should be replaced with a tab character.</p>

<h2 id="step-7-animating-the-characters-lip-sync">Step 7: Animating the Characters (Lip-Sync)</h2>

<p>With the visual characters and the audio generated, the final creative step is animating the hosts with realistic lip synchronization.</p>

<p>I utilized the <strong>InfiniteTalk</strong> model for this task, feeding it the scene image with the characters from Step 3 and the separated audio files from Step 6.</p>

<p>Just upload an audio file for each speaker, along with their corresponding image, to the workflow. The model will then generate a lip-synced video.</p>

<p>Here is the prompt:</p>

<blockquote>
  <p>A man and a woman are talking to each other.</p>
</blockquote>

<p>Example of the animation:</p>

<p><img src="/assets/images/podcast-infinite-talk.gif" alt="infinite-talk" title="Infinite Talk Animation" width="600" /></p>

<p>The generation time for each <strong>one-minute</strong> segment was about <strong>60 minutes</strong> on my AMD Radeon AI PRO R9700.</p>

<h2 id="step-8-assembling-the-final-production">Step 8: Assembling the Final Production</h2>

<p>The last stage is combining all the assets we generated into a cohesive whole.</p>

<p>I used the open-source video editor <strong>Kdenlive</strong> to assemble the project:</p>
<ol>
  <li><strong>Stitching Segments:</strong> Combining the five animated segments.</li>
  <li><strong>Transitions:</strong> Adding the 3-second B-rolls between the main discussion segments.</li>
  <li><strong>Final Touches:</strong> Adding the animated intro clip and the dim-lit outro scene with credits.</li>
  <li><strong>Audio Sync:</strong> Integrating the intro/outro music and B-roll audio.</li>
</ol>

<p><img src="/assets/images/kdenlive-podcast-project.png" alt="kdenlive" title="Kdenlive Podcast Project" /></p>

<p>You can download the full Kdenlive project, along with all the assets and ComfyUI workflows, here on my GitHub: <a href="https://github.com/meefik/humanless-podcast">meefik/humanless-podcast</a>.</p>

<h2 id="conclusion">Conclusion</h2>

<p>This project demonstrates that creating a complete, high-quality AI-generated media asset is absolutely possible using only open-source, self-hosted AI models and tools. It’s not an one-click solution, and the quality still requires a creative human touch, but the cost is only your time and the computational power of your local machine.</p>

<p>It was an exciting journey pushing the boundaries of what local AI can achieve. I hope this inspires you to explore similar projects and experiment with the incredible capabilities of open-source AI. The “Humanless Podcast” is a testament to the power of local AI—and I encourage you to try to create your own!</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="comfyui" /><summary type="html"><![CDATA[Creating a full AI-generated podcast using only open-source models and tools on a local machine.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://meefik.github.io/assets/images/podcast-title.png" /><media:content medium="image" url="https://meefik.github.io/assets/images/podcast-title.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">LLM performance on AMD Radeon AI PRO R9700</title><link href="https://meefik.github.io/2025/11/15/llms-performance-on-amdgpu/" rel="alternate" type="text/html" title="LLM performance on AMD Radeon AI PRO R9700" /><published>2025-11-15T18:00:00+00:00</published><updated>2025-11-15T18:00:00+00:00</updated><id>https://meefik.github.io/2025/11/15/llms-performance-on-amdgpu</id><content type="html" xml:base="https://meefik.github.io/2025/11/15/llms-performance-on-amdgpu/"><![CDATA[<p>Recently, I acquired an <a href="https://www.amd.com/en/products/graphics/workstations/radeon-ai-pro/ai-9000-series/amd-radeon-ai-pro-r9700.html">AMD Radeon AI PRO R9700</a> to enhance my machine learning and development setup. It is a powerful GPU designed for professional workloads, including machine learning and AI applications. In this post, we explore the performance of large language models (LLMs) on the R9700, highlighting its capabilities and benchmarks.</p>

<p><img src="/assets/images/llm-performance.png" alt="chart" title="LLM performance using Ollama" /></p>

<!--more-->

<h2 id="hardware-overview">Hardware overview</h2>

<p>The PC used for testing is equipped with the following specifications:</p>

<ul>
  <li><strong>Motherboard</strong>: B550 AORUS ELITE AX V2</li>
  <li><strong>GPU</strong>: AMD Radeon AI PRO R9700 (32 GB VRAM, RDNA 4)</li>
  <li><strong>CPU</strong>: AMD Ryzen 9 5950X (16 cores / 32 threads)</li>
  <li><strong>RAM</strong>: 64 GB DDR4-2666</li>
  <li><strong>Storage</strong>: NVMe Samsung SSD 970 EVO Plus 1TB</li>
</ul>

<h2 id="setup-environment">Setup environment</h2>

<p>For testing LLM performance we set up the following environment:</p>

<ul>
  <li><a href="https://docs.docker.com/compose/">Docker Compose</a></li>
  <li><a href="https://ollama.com">Ollama with ROCm</a></li>
  <li><a href="https://github.com/open-webui/open-webui">Open WebUI</a></li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> file used for the setup is as follows:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">ollama</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ollama/ollama:rocm</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">11434:11434</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ollama:/root/.ollama</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">HSA_OVERRIDE_GFX_VERSION=12.0.0'</span>
    <span class="na">devices</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">/dev/kfd'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">/dev/dri'</span>
    <span class="na">tty</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>

  <span class="na">open-webui</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">ghcr.io/open-webui/open-webui:main</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">open-webui:/app/backend/data</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">ollama</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">8080:8080</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">OLLAMA_BASE_URL=http://ollama:11434'</span>
      <span class="pi">-</span> <span class="s1">'</span><span class="s">WEBUI_SECRET_KEY='</span>
    <span class="na">extra_hosts</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">host.docker.internal:host-gateway</span>
    <span class="na">restart</span><span class="pi">:</span> <span class="s">unless-stopped</span>

<span class="na">volumes</span><span class="pi">:</span>
  <span class="na">ollama</span><span class="pi">:</span>
  <span class="na">open-webui</span><span class="pi">:</span>
</code></pre></div></div>

<p>Run these services with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose up <span class="nt">-d</span>
</code></pre></div></div>

<h2 id="benchmark-results">Benchmark results</h2>

<p>I used the following prompt for testing text generation performance:</p>

<p><code class="language-plaintext highlighter-rouge">Tell me a story about a brave knight who saves a village from a dragon.</code></p>

<p>For vision models that understand images, I used this image with the following prompt:</p>

<p><code class="language-plaintext highlighter-rouge">What is in this picture?</code></p>

<p><img src="/assets/images/meefik-at-work.png" alt="image-example" /></p>

<h3 id="amd-radeon-ai-pro-r9700-32-gb-vram-rdna-4">AMD Radeon AI PRO R9700 (32 GB VRAM, RDNA 4)</h3>

<p>Here are the benchmark results for models with text generation only:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>VRAM usage</th>
      <th>Prompt</th>
      <th>Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>mistral:7b</td>
      <td>6 GB</td>
      <td>414 tokens/sec</td>
      <td>80 tokens/sec</td>
    </tr>
    <tr>
      <td>llama3.1:8b</td>
      <td>7 GB</td>
      <td>386 tokens/sec</td>
      <td>77 tokens/sec</td>
    </tr>
    <tr>
      <td>phi4:14b</td>
      <td>12 GB</td>
      <td>213 tokens/sec</td>
      <td>50 tokens/sec</td>
    </tr>
    <tr>
      <td>gpt-oss:20b</td>
      <td>13 GB</td>
      <td>704 tokens/sec</td>
      <td>91 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:27b</td>
      <td>19 GB</td>
      <td>207 tokens/sec</td>
      <td>27 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-coder:30b</td>
      <td>18 GB</td>
      <td>250 tokens/sec</td>
      <td>75 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3:32b</td>
      <td>21 GB</td>
      <td>179 tokens/sec</td>
      <td>23 tokens/sec</td>
    </tr>
    <tr>
      <td>deepseek-r1:32b</td>
      <td>22 GB</td>
      <td>99 tokens/sec</td>
      <td>23 tokens/sec</td>
    </tr>
  </tbody>
</table>

<p>For vision models that understand images, here are the results:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>VRAM usage</th>
      <th>Prompt</th>
      <th>Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>moondream:1.8b</td>
      <td>3 GB</td>
      <td>2156 tokens/sec</td>
      <td>188 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:4b</td>
      <td>5 GB</td>
      <td>1316 tokens/sec</td>
      <td>107 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3n:e4b</td>
      <td>8 GB</td>
      <td>367 tokens/sec</td>
      <td>58 tokens/sec</td>
    </tr>
    <tr>
      <td>llava:7b</td>
      <td>6 GB</td>
      <td>515 tokens/sec</td>
      <td>83 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-vl:8b</td>
      <td>11 GB</td>
      <td>423 tokens/sec</td>
      <td>73 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:27b</td>
      <td>19 GB</td>
      <td>171 tokens/sec</td>
      <td>27 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-vl:32b</td>
      <td>26 GB</td>
      <td>132 tokens/sec</td>
      <td>24 tokens/sec</td>
    </tr>
    <tr>
      <td>llava:34b</td>
      <td>22 GB</td>
      <td>129 tokens/sec</td>
      <td>24 tokens/sec</td>
    </tr>
  </tbody>
</table>

<h3 id="macbook-pro-m4-max-36-gb-ram-14-cores">MacBook Pro M4 Max (36 GB RAM, 14 cores)</h3>

<p>Here are the benchmark results for models with text generation only:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Prompt</th>
      <th>Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>mistral:7b</td>
      <td>208 tokens/sec</td>
      <td>66 tokens/sec</td>
    </tr>
    <tr>
      <td>llama3.1:8b</td>
      <td>219 tokens/sec</td>
      <td>61 tokens/sec</td>
    </tr>
    <tr>
      <td>phi4:14b</td>
      <td>91 tokens/sec</td>
      <td>32 tokens/sec</td>
    </tr>
    <tr>
      <td>gpt-oss:20b</td>
      <td>62 tokens/sec</td>
      <td>65 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:27b</td>
      <td>11 tokens/sec</td>
      <td>15 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-coder:30b</td>
      <td>88 tokens/sec</td>
      <td>63 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3:32b</td>
      <td>57 tokens/sec</td>
      <td>11 tokens/sec</td>
    </tr>
    <tr>
      <td>deepseek-r1:32b</td>
      <td>48 tokens/sec</td>
      <td>12 tokens/sec</td>
    </tr>
  </tbody>
</table>

<p>For vision models that understand images, here are the results:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Prompt</th>
      <th>Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>moondream:1.8b</td>
      <td>1706 tokens/sec</td>
      <td>180 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:4b</td>
      <td>995 tokens/sec</td>
      <td>81 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3n:e4b</td>
      <td>39 tokens/sec</td>
      <td>46 tokens/sec</td>
    </tr>
    <tr>
      <td>llava:7b</td>
      <td>640 tokens/sec</td>
      <td>65 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-vl:8b</td>
      <td>246 tokens/sec</td>
      <td>56 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:27b</td>
      <td>183 tokens/sec</td>
      <td>18 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-vl:32b</td>
      <td>76 tokens/sec</td>
      <td>15 tokens/sec</td>
    </tr>
    <tr>
      <td>llava:34b</td>
      <td>143 tokens/sec</td>
      <td>16 tokens/sec</td>
    </tr>
  </tbody>
</table>

<h3 id="amd-ryzen-9-5950x-16-cores32-threads">AMD Ryzen 9 5950X (16 cores/32 threads)</h3>

<p>This is the CPU-only performance without GPU acceleration.</p>

<p>Here are the benchmark results for models with text generation only:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Prompt</th>
      <th>Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>mistral:7b</td>
      <td>61 tokens/sec</td>
      <td>8 tokens/sec</td>
    </tr>
    <tr>
      <td>llama3.1:8b</td>
      <td>62 tokens/sec</td>
      <td>7 tokens/sec</td>
    </tr>
    <tr>
      <td>phi4:14b</td>
      <td>32 tokens/sec</td>
      <td>4 tokens/sec</td>
    </tr>
    <tr>
      <td>gpt-oss:20b</td>
      <td>94 tokens/sec</td>
      <td>8 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:27b</td>
      <td>18 tokens/sec</td>
      <td>2 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-coder:30b</td>
      <td>79 tokens/sec</td>
      <td>15 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3:32b</td>
      <td>15 tokens/sec</td>
      <td>2 tokens/sec</td>
    </tr>
    <tr>
      <td>deepseek-r1:32b</td>
      <td>14 tokens/sec</td>
      <td>2 tokens/sec</td>
    </tr>
  </tbody>
</table>

<p>For vision models that understand images, here are the results:</p>

<table>
  <thead>
    <tr>
      <th>Model</th>
      <th>Prompt</th>
      <th>Response</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>moondream:1.8b</td>
      <td>293 tokens/sec</td>
      <td>27 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:4b</td>
      <td>63 tokens/sec</td>
      <td>7 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3n:e4b</td>
      <td>82 tokens/sec</td>
      <td>10 tokens/sec</td>
    </tr>
    <tr>
      <td>llava:7b</td>
      <td>65 tokens/sec</td>
      <td>8 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-vl:8b</td>
      <td>26 tokens/sec</td>
      <td>6 tokens/sec</td>
    </tr>
    <tr>
      <td>gemma3:27b</td>
      <td>18 tokens/sec</td>
      <td>2 tokens/sec</td>
    </tr>
    <tr>
      <td>qwen3-vl:32b</td>
      <td>10 tokens/sec</td>
      <td>1 tokens/sec</td>
    </tr>
    <tr>
      <td>llava:34b</td>
      <td>14 tokens/sec</td>
      <td>2 tokens/sec</td>
    </tr>
  </tbody>
</table>

<h2 id="conclusion">Conclusion</h2>

<p>The AMD Radeon AI PRO R9700 demonstrates strong performance across a variety of large language models, handling both text-only and vision-capable models effectively. With its substantial VRAM and robust architecture, the R9700 is well-suited for professional AI workloads, making it a compelling choice for developers and researchers working with LLMs. Now, I can take full advantage of AMD’s capabilities for my AI projects and code with local LLMs!</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="amdgpu" /><category term="llm" /><summary type="html"><![CDATA[Benchmarking large language models (LLMs) on AMD's Radeon AI PRO R9700 GPU using Ollama and Open WebUI.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://meefik.github.io/assets/images/llm-performance.png" /><media:content medium="image" url="https://meefik.github.io/assets/images/llm-performance.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Serverless WebRTC conferencing with E2E encryption</title><link href="https://meefik.github.io/2025/10/05/webrtc-p2p-conferencing/" rel="alternate" type="text/html" title="Serverless WebRTC conferencing with E2E encryption" /><published>2025-10-05T18:00:00+00:00</published><updated>2025-10-05T18:00:00+00:00</updated><id>https://meefik.github.io/2025/10/05/webrtc-p2p-conferencing</id><content type="html" xml:base="https://meefik.github.io/2025/10/05/webrtc-p2p-conferencing/"><![CDATA[<p>Building reliable, privacy-respecting peer-to-peer conferencing can be surprisingly simple when you split responsibilities cleanly: media transport (WebRTC) and signaling (a tiny transport for exchanging SDP and ICE). I built <a href="https://github.com/meefik/p2p">a minimal library</a> to demonstrate that split and to enable serverless workflows using whatever signaling channel you prefer — from in-memory drivers for demos to NATS-based pub/sub for distributed apps.</p>

<p>This post describes the library’s purpose, core design, how to use it, and a practical example of a <a href="https://nats.io">NATS</a> signaling driver with end-to-end encryption using the browser <a href="https://developer.mozilla.org/docs/Web/API/Web_Crypto_API">Web Crypto API</a>.</p>

<p><img src="/assets/images/p2p-demo.png" alt="demo" title="Peer-to-Peer Video Conference" /></p>

<p>Just try it out: <a href="https://talk.meefik.dev/">live demo</a> | <a href="https://github.com/meefik/p2p">source code</a></p>

<!--more-->

<h2 id="why-this-library">Why this library</h2>

<p>The library focuses on three goals:</p>

<ul>
  <li>Minimal surface area: two primitives (Sender and Receiver) that cover the common conferencing pattern: one broadcaster, many receivers.</li>
  <li>Signaling-agnostic: you provide a small driver implementing on/off/emit and the library works with any transport.</li>
  <li>Practical privacy: support optional E2E encryption at the signaling layer so session offers/answers and candidates are not exposed in plaintext on the bus.</li>
</ul>

<h2 id="design-overview">Design overview</h2>

<p>At its core, p2p is small:</p>

<ul>
  <li>Sender: creates outgoing RTCPeerConnections, publishes a local MediaStream and optional per-peer RTCDataChannels, and emits offers to receivers.</li>
  <li>Receiver: listens for offers, answers them, and surfaces remote streams and incoming data messages to the application.</li>
</ul>

<p>Signaling expectations are intentionally simple: drivers produce messages scoped to namespaces (arrays/keys). The library uses namespaces such as [“sender”, room], [“receiver”, room, id], etc. Messages include typed payloads (invoke, offer, answer, candidate, sync, dispose).</p>

<h2 id="quick-start">Quick start</h2>

<p><strong>1.</strong> Install the library:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>npm <span class="nb">install </span>p2p
</code></pre></div></div>

<p><strong>2.</strong> Implement a signaling driver that supports on/off/emit.</p>

<p>Here’s a minimal conceptual example:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nx">MyDriver</span> <span class="p">{</span>
  <span class="nx">on</span><span class="p">(</span><span class="nx">namespace</span><span class="p">,</span> <span class="nx">handler</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
  <span class="nx">off</span><span class="p">(</span><span class="nx">namespace</span><span class="p">,</span> <span class="nx">handler</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
  <span class="nx">emit</span><span class="p">(</span><span class="nx">namespace</span><span class="p">,</span> <span class="nx">message</span><span class="p">)</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Instantiate your driver:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">driver</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">MyDriver</span><span class="p">();</span>
</code></pre></div></div>

<p><strong>3.</strong> Start a Receiver in the same room to discover and receive streams.</p>

<p>Instantiate Receiver with the same driver:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">receiver</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Receiver</span><span class="p">({</span> <span class="nx">driver</span> <span class="p">});</span>
<span class="nx">receiver</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">stream</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">stream</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">detail</span><span class="p">;</span>
  <span class="c1">// handle incoming MediaStream</span>
<span class="p">});</span>
<span class="nx">receiver</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">connect</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">detail</span><span class="p">;</span>
  <span class="c1">// handle peer connection established</span>
<span class="p">});</span>
<span class="nx">receiver</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">dispose</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="p">{</span> <span class="nx">id</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">detail</span><span class="p">;</span>
  <span class="c1">// handle peer disconnection</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Start the receiver in the same room:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">receiver</span><span class="p">.</span><span class="nx">start</span><span class="p">({</span> <span class="na">room</span><span class="p">:</span> <span class="dl">'</span><span class="s1">demo-room</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<p><strong>4.</strong> Create and start a Sender to broadcast local media.</p>

<p>Instantiate Sender with your driver and options:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">sender</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Sender</span><span class="p">({</span> <span class="nx">driver</span> <span class="p">});</span>
</code></pre></div></div>

<p>Start the sender with the stream and a room name:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">stream</span> <span class="o">=</span> <span class="k">await</span> <span class="nb">navigator</span><span class="p">.</span><span class="nx">mediaDevices</span><span class="p">.</span><span class="nx">getUserMedia</span><span class="p">({</span>
  <span class="na">video</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span>
  <span class="na">audio</span><span class="p">:</span> <span class="kc">true</span>
<span class="p">});</span>
<span class="nx">sender</span><span class="p">.</span><span class="nx">start</span><span class="p">({</span> <span class="nx">stream</span><span class="p">,</span> <span class="na">room</span><span class="p">:</span> <span class="dl">'</span><span class="s1">demo-room</span><span class="dl">'</span> <span class="p">});</span>
</code></pre></div></div>

<h2 id="nats-as-a-signaling-transport-with-e2e-encryption">NATS as a signaling transport with E2E encryption</h2>

<p><a href="https://nats.io">NATS</a> is a great lightweight pub/sub for distributed signaling. The demo repository includes a full driver implementation at <a href="https://github.com/meefik/p2p/blob/main/demo/driver/nats.js">demo/driver/nats.js</a>; below is the compact approach and key ideas used there.</p>

<p>Here’s a simple driver implementation using the <a href="https://npmjs.com/package/nats.ws">nats.ws</a> module:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">connect</span><span class="p">,</span> <span class="nx">StringCodec</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">nats.ws</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">sc</span> <span class="o">=</span> <span class="nx">StringCodec</span><span class="p">();</span>

<span class="kd">class</span> <span class="nx">NatsDriver</span> <span class="kd">extends</span> <span class="nb">Map</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">({</span> <span class="nx">servers</span> <span class="p">}</span> <span class="o">=</span> <span class="p">{})</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">();</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">servers</span> <span class="o">=</span> <span class="nx">servers</span> <span class="o">||</span> <span class="p">[</span><span class="dl">'</span><span class="s1">wss://demo.nats.io:8443</span><span class="dl">'</span><span class="p">];</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">open</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">nc</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">connect</span><span class="p">({</span> <span class="na">servers</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">servers</span><span class="p">,</span> <span class="na">noEcho</span><span class="p">:</span> <span class="kc">true</span> <span class="p">});</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">close</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">await</span> <span class="k">this</span><span class="p">.</span><span class="nx">nc</span><span class="p">.</span><span class="nx">drain</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="nx">on</span><span class="p">(</span><span class="nx">namespace</span><span class="p">,</span> <span class="nx">handler</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">ns</span> <span class="o">=</span> <span class="nx">namespace</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">sub</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">nc</span><span class="p">.</span><span class="nx">subscribe</span><span class="p">(</span><span class="nx">ns</span><span class="p">,</span> <span class="p">{</span>
      <span class="na">callback</span><span class="p">:</span> <span class="k">async</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">msg</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="k">return</span> <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="nx">err</span><span class="p">);</span>
        <span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">sc</span><span class="p">.</span><span class="nx">decode</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">data</span><span class="p">));</span>
        <span class="nx">handler</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span>
      <span class="p">},</span>
    <span class="p">});</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">ns</span><span class="p">))</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">ns</span><span class="p">,</span> <span class="k">new</span> <span class="nb">Map</span><span class="p">());</span>
    <span class="p">}</span>
    <span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">ns</span><span class="p">).</span><span class="kd">set</span><span class="p">(</span><span class="nx">handler</span><span class="p">,</span> <span class="nx">sub</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="nx">off</span><span class="p">(</span><span class="nx">namespace</span><span class="p">,</span> <span class="nx">handler</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">ns</span> <span class="o">=</span> <span class="nx">namespace</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">sub</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">ns</span><span class="p">)?.</span><span class="kd">get</span><span class="p">(</span><span class="nx">handler</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">sub</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">sub</span><span class="p">.</span><span class="nx">unsubscribe</span><span class="p">();</span>
      <span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">ns</span><span class="p">).</span><span class="k">delete</span><span class="p">(</span><span class="nx">handler</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="k">this</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">ns</span><span class="p">)?.</span><span class="nx">size</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">.</span><span class="k">delete</span><span class="p">(</span><span class="nx">ns</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="k">async</span> <span class="nx">emit</span><span class="p">(</span><span class="nx">namespace</span><span class="p">,</span> <span class="nx">message</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">ns</span> <span class="o">=</span> <span class="nx">namespace</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">nc</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">sc</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">message</span><span class="p">));</span>
      <span class="k">this</span><span class="p">.</span><span class="nx">nc</span><span class="p">.</span><span class="nx">publish</span><span class="p">(</span><span class="nx">ns</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you want the signaling payloads to be encrypted end-to-end (so the NATS server only sees opaque blobs), you can apply symmetric encryption on top of the driver.</p>

<p>High-level strategy:</p>
<ul>
  <li>Derive an AES-GCM key from a shared passphrase (or pre-shared secret) using SHA-256.</li>
  <li>For every outbound message: encode JSON → encrypt with AES-GCM (random IV) → publish binary payload.</li>
  <li>For inbound messages: decrypt using AES-GCM with the same key → parse JSON → deliver to handler.</li>
  <li>Keep namespaces and message types unchanged; only payload bytes are encrypted.</li>
</ul>

<p>Here’s a compact encryption helper (browser) using <a href="https://developer.mozilla.org/docs/Web/API/Web_Crypto_API">Web Crypto API</a>.</p>

<p>Derive AES-GCM CryptoKey from passphrase via SHA-256:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">createEncryptionKey</span><span class="p">(</span><span class="nx">secret</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">secretHash</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">digest</span><span class="p">(</span>
    <span class="dl">'</span><span class="s1">SHA-256</span><span class="dl">'</span><span class="p">,</span>
    <span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="nx">secret</span><span class="p">),</span>
  <span class="p">);</span>
  <span class="k">return</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">importKey</span><span class="p">(</span>
    <span class="dl">'</span><span class="s1">raw</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">secretHash</span><span class="p">,</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">AES-GCM</span><span class="dl">'</span> <span class="p">},</span>
    <span class="kc">false</span><span class="p">,</span>
    <span class="p">[</span><span class="dl">'</span><span class="s1">encrypt</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">decrypt</span><span class="dl">'</span><span class="p">],</span>
  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Prepend a 12-byte IV + ciphertext:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">encrypt</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="nx">cryptoKey</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">iv</span> <span class="o">=</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">getRandomValues</span><span class="p">(</span><span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="mi">12</span><span class="p">));</span>
  <span class="kd">const</span> <span class="nx">ciphertext</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span>
    <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">encrypt</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">AES-GCM</span><span class="dl">'</span><span class="p">,</span> <span class="nx">iv</span> <span class="p">},</span> <span class="nx">cryptoKey</span><span class="p">,</span> <span class="nx">payload</span><span class="p">),</span>
  <span class="p">);</span>
  <span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">iv</span><span class="p">.</span><span class="nx">byteLength</span> <span class="o">+</span> <span class="nx">ciphertext</span><span class="p">.</span><span class="nx">byteLength</span><span class="p">);</span>
  <span class="nx">data</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">iv</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span>
  <span class="nx">data</span><span class="p">.</span><span class="kd">set</span><span class="p">(</span><span class="nx">ciphertext</span><span class="p">,</span> <span class="nx">iv</span><span class="p">.</span><span class="nx">byteLength</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">data</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Extract IV and decrypt:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">decrypt</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="nx">cryptoKey</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">iv</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">12</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">ct</span> <span class="o">=</span> <span class="nx">data</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">12</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">decrypt</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">AES-GCM</span><span class="dl">'</span><span class="p">,</span> <span class="nx">iv</span> <span class="p">},</span> <span class="nx">cryptoKey</span><span class="p">,</span> <span class="nx">ct</span><span class="p">);</span>
  <span class="k">return</span> <span class="nx">payload</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Example usage (conceptual snippet):</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Create a key from a human passphrase</span>
<span class="kd">const</span> <span class="nx">key</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">createEncryptionKey</span><span class="p">(</span><span class="dl">'</span><span class="s1">room-secret-passphrase</span><span class="dl">'</span><span class="p">);</span>

<span class="c1">// Encrypt message (payload is Uint8Array)</span>
<span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="dl">'</span><span class="s1">Secret message</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">data</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">encrypt</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="nx">key</span><span class="p">);</span>

<span class="c1">// returns Uint8Array with IV+ciphertext</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">encrypted data</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>

<span class="c1">// Decrypt message</span>
<span class="kd">const</span> <span class="nx">decryptedBytes</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">decrypt</span><span class="p">(</span><span class="nx">data</span><span class="p">,</span> <span class="nx">key</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">message</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TextDecoder</span><span class="p">().</span><span class="nx">decode</span><span class="p">(</span><span class="nx">decryptedBytes</span><span class="p">);</span>

<span class="c1">// returns original message</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">decrypted message</span><span class="dl">'</span><span class="p">,</span> <span class="nx">message</span><span class="p">);</span>
</code></pre></div></div>

<p>Integrate encryption into the NATS driver by wrapping emit/on methods to encrypt/decrypt payloads. Here is diff of the modified methods:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">-  async open() {
</span><span class="gi">+  async open(secret) {
</span>     this.nc = await connect({ servers: this.servers, noEcho: true });
<span class="gi">+    if (secret) {
+      this.cryptoKey = await createEncryptionKey(secret);
+    }
</span>   }
 
   async close() {
     const sub = this.nc.subscribe(ns, {
       callback: async (err, msg) =&gt; {
         if (err) return console.error(err);
<span class="gd">-        const payload = JSON.parse(sc.decode(msg.data));
</span><span class="gi">+        let data = msg.data;
+        if (this.cryptoKey) {
+          data = await decrypt(data, this.cryptoKey);
+        }
+        const payload = JSON.parse(sc.decode(data));
</span>         handler(payload);
       },
     });
    if (!this.has(ns)) {
      this.set(ns, new Map());
    }
    this.get(ns).set(handler, sub);
  }

   async emit(namespace, message) {
     const ns = namespace.join(':');
     if (this.nc) {
<span class="gd">-      const data = sc.encode(JSON.stringify(message));
</span><span class="gi">+      let data = sc.encode(JSON.stringify(message));
+      if (this.cryptoKey) {
+        data = await encrypt(data, this.cryptoKey);
+      }
</span>       this.nc.publish(ns, data);
     }
   }
</code></pre></div></div>

<p>Operational considerations:</p>

<ul>
  <li>If you run NATS in production, use authentication/authorization and TLS.</li>
  <li>For real-world NAT traversal include TURN servers in iceServers.</li>
  <li>For larger conferences, consider SFU architecture rather than pure p2p (p2p scales poorly with N participants).</li>
  <li>The encrypted signaling only protects SDP and candidates; media still flows directly between peers (or via TURN) and should be protected by SRTP (it’s part of WebRTC).</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>This project shows how a small, well-factored library can enable flexible, serverless peer-to-peer conferencing while giving you control over signaling and privacy. The NATS driver with E2E encryption is a practical option for distributed systems where you want to keep signaling private without a heavy backend.</p>

<p>See the <a href="https://meefik.dev/p2p/">live demo</a> for runnable examples and the full NATS driver implementation: <a href="https://github.com/meefik/p2p/blob/main/demo/driver/nats.js">demo/driver/nats.js</a>.</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="webrtc" /><category term="javascript" /><summary type="html"><![CDATA[Building a peer-to-peer WebRTC conferencing system with NATS for signaling and end-to-end encryption using the P2P library.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://meefik.github.io/assets/images/p2p-demo.png" /><media:content medium="image" url="https://meefik.github.io/assets/images/p2p-demo.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to reduce AMD GPU power consumption on Linux</title><link href="https://meefik.github.io/2025/09/20/amdgpu-power-consumption/" rel="alternate" type="text/html" title="How to reduce AMD GPU power consumption on Linux" /><published>2025-09-20T18:00:00+00:00</published><updated>2025-09-20T18:00:00+00:00</updated><id>https://meefik.github.io/2025/09/20/amdgpu-power-consumption</id><content type="html" xml:base="https://meefik.github.io/2025/09/20/amdgpu-power-consumption/"><![CDATA[<p>If you have a PC running Linux with an AMD GPU, you can change your GPU performance level. By default, <a href="https://rocm.docs.amd.com/projects/install-on-linux/en/latest/reference/user-kernel-space-compat-matrix.html">the AMDGPU driver</a> uses the “auto” performance level. But if you don’t need high performance, you can set it to “low” to reduce power consumption, heat generation, and fan noise.</p>

<p><img src="/assets/images/amdgpu-power-level.png" alt="amd_gpu_power_level" title="AMD GPU power consumption before and after changing performance level" /></p>

<p>On my system this change reduced the GPU power consumption from 30W to 15W in idle state and completely eliminated fan spinning.</p>

<!--more-->

<p>You can check the current performance level with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /sys/class/drm/card0/device/power_dpm_force_performance_level
</code></pre></div></div>

<p>You can change the performance level on the fly with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"low"</span> | <span class="nb">sudo tee</span> /sys/class/drm/card0/device/power_dpm_force_performance_level
<span class="nb">echo</span> <span class="s2">"auto"</span> | <span class="nb">sudo tee</span> /sys/class/drm/card0/device/power_dpm_force_performance_level
</code></pre></div></div>

<p>To make this change permanent, create a udev rule:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> <span class="o">&lt;&lt;</span> <span class="no">EOF</span><span class="sh"> | sudo tee /etc/udev/rules.d/30-amdgpu-low-power.rules
SUBSYSTEM=="pci", DRIVER=="amdgpu", ATTR{power_dpm_force_performance_level}="low"
</span><span class="no">EOF
</span></code></pre></div></div>

<p>After that, the AMD GPU will use the “low” performance level on each boot.</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="amdgpu" /><category term="linux" /><summary type="html"><![CDATA[A simple guide to lower AMD GPU power consumption by changing performance levels on Linux systems.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://meefik.github.io/assets/images/amdgpu-power-level.png" /><media:content medium="image" url="https://meefik.github.io/assets/images/amdgpu-power-level.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The story of a (not so) necessary optimization</title><link href="https://meefik.github.io/2025/04/20/mongodb-optimization/" rel="alternate" type="text/html" title="The story of a (not so) necessary optimization" /><published>2025-04-20T10:00:00+00:00</published><updated>2025-04-20T10:00:00+00:00</updated><id>https://meefik.github.io/2025/04/20/mongodb-optimization</id><content type="html" xml:base="https://meefik.github.io/2025/04/20/mongodb-optimization/"><![CDATA[<p>I am using <a href="https://nodejs.org/api/cluster.html">Node.js Cluster</a> app with <a href="https://www.mongodb.com/docs/manual/replication/">MongoDB Replica Set</a> in one of my projects. In the server architecture of the system, the <a href="https://www.mongodb.com/docs/manual/changeStreams/">MongoDB Change Streams</a> mechanism is used to implement the horizontal scaling of real-time functionality (video communication, chats, notifications), which allows subscribing to changes occurring in the database. Previously, instead of this mechanism, I used data exchange over UDP directly between the application server hosts until our hoster, for an unknown reason, began to lose a significant portion of packets. Because of this, I had to abandon this method. For the last couple of months, I’ve been wondering how to optimize the operation of this mechanism in MongoDB, or even abandon it in favor of connecting an additional component like <a href="https://redis.io/docs/latest/develop/interact/pubsub/">Redis Pub/Sub</a>. But without a particular need, I didn’t want to multiply entities, <a href="https://en.wikipedia.org/wiki/Occam%27s_razor">Occam’s Razor</a>, you know. Besides, figuring out what’s already there isn’t a bad idea to start with.</p>

<p><img src="/assets/images/mongodb-nodejs-scale.jpg" alt="architecture" title="MongoDB + Node.js scale" /></p>

<!--more-->

<p>First of all, I thought, since our DBMS cluster consists of two hosts (primary and secondary), it would be nice to switch tracking changes via Change Streams to the Secondary host, because millisecond latency in data synchronization between hosts isn’t critical for us here. No problem, done! This allowed us to distribute the load between the cluster hosts, but it still remained uneven. As it turned out, Change Streams on the Secondary host created more CPU load than all other operations on the Primary host. Studying the official documentation, forums, and even AI didn’t provide an understanding of the performance of this function, although the principle of operation eventually became clear.</p>

<p>In practice, experiments revealed that one of the features of Change Streams is that despite the fact that you can subscribe to changes only in a specific collection, it seems that under the hood, all changes across the entire database are collected and then filtered. This leads to the fact that any data changes in the database create an additional, albeit small, load. Digging further, I found an alternative mechanism for tracking the addition of new documents to a collection (which is exactly what we need), called <a href="https://www.mongodb.com/docs/manual/core/tailable-cursors/">Tailable Cursor</a>. It uses the same <a href="https://www.mongodb.com/docs/manual/core/replica-set-oplog/">oplog</a> as Change Streams, but it seems to be structured a bit differently. Unlike Change Streams, it reacts to the addition of documents only to a specific <a href="https://www.mongodb.com/docs/manual/core/capped-collections/">capped collection</a> and doesn’t create CPU load when data changes in other collections. Oh, so that was an option? Okay, done!</p>

<p>Synthetic tests showed a 20% performance increase, which is already good. However, in production, the load on the Secondary node could exceed the Primary by 2 to 5 times! So, there’s still something else going on. I conducted a synthetic test for adding documents to a collection (up to 1000 ops!) that is being monitored. It turned out that the load when adding documents to the monitored collection is 5% higher than to an unmonitored one. But then, it gets more interesting. Each new observer increases the load on the host by roughly 50%! And the more workers the application server has, the greater the load on the database will be. This can be worked with; we just need to reduce the number of listeners. Piece of cake, the new synchronization architecture is done, where within the same host, Node.js cluster workers communicate via the <a href="https://en.wikipedia.org/wiki/Inter-process_communication">IPC</a> (as in the case of working without a Replica Set), and hosts communicate through the Tailable Cursor mechanism in MongoDB. Thus, vertical scaling does not use the database, and horizontal scaling does, but it is limited by the number of application server hosts, not the number of its workers.</p>

<p>In the end, there were three optimizations:</p>

<ol>
  <li>shifting the load to the secondary host;</li>
  <li>replacing change streams with tailable cursor;</li>
  <li>reducing the number of listeners (1 listener - 1 host, instead of 1 listener - 1 worker).</li>
</ol>

<p>In the end, this journey highlighted the importance of not just blindly implementing solutions, but also understanding the underlying technology and continuously seeking improvements. What initially felt like a small tweak turned into a series of optimizations that significantly impacted the system’s efficiency. It’s a good reminder that sometimes, the “unnecessary” optimization can lead to surprisingly valuable insights and improvements.</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="mongodb" /><summary type="html"><![CDATA[Exploring optimizations in MongoDB Change Streams and Tailable Cursors to improve performance in a Node.js Cluster application.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://meefik.github.io/assets/images/mongodb-nodejs-scale.jpg" /><media:content medium="image" url="https://meefik.github.io/assets/images/mongodb-nodejs-scale.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Tailwind CSS v4 and the Shadow DOM</title><link href="https://meefik.github.io/2025/03/19/tailwindcss-and-shadow-dom/" rel="alternate" type="text/html" title="Tailwind CSS v4 and the Shadow DOM" /><published>2025-03-19T10:00:00+00:00</published><updated>2025-03-19T10:00:00+00:00</updated><id>https://meefik.github.io/2025/03/19/tailwindcss-and-shadow-dom</id><content type="html" xml:base="https://meefik.github.io/2025/03/19/tailwindcss-and-shadow-dom/"><![CDATA[<p>Tailwind CSS v4 was recently released, and with it came a problem when using the Shadow DOM. You can find the issue here: <a href="https://github.com/tailwindlabs/tailwindcss/issues/15005">tailwindlabs/tailwindcss#15005</a>.</p>

<p>Tailwind v4 uses <code class="language-plaintext highlighter-rouge">@property</code> to define defaults for custom properties. Currently, shadow roots do not support <code class="language-plaintext highlighter-rouge">@property</code>. Although it was explicitly disallowed in the spec, there is ongoing discussion about adding support: <a href="https://github.com/w3c/css-houdini-drafts/pull/1085">w3c/css-houdini-drafts#1085</a>.</p>

<p>It is unknown if the developers will fix this issue. In this post, we will consider workarounds to address it.</p>

<!--more-->

<h2 id="workaround-1-global-property-declarations-with-vite">Workaround 1: Global @property Declarations with Vite</h2>

<p>One straightforward approach is to declare your <code class="language-plaintext highlighter-rouge">@property</code> rules in the main document scope, effectively making them available globally. While this approach offers less encapsulation, it works because custom properties inherit down the DOM tree by default.</p>

<p>Tailwind doesn’t currently offer a built-in way to extract just the <code class="language-plaintext highlighter-rouge">@property</code> definitions. However, if you’re using Vite, you can implement a simple build-time transformation to achieve this. This solution is described <a href="https://github.com/tailwindlabs/tailwindcss/issues/15005">here</a>.</p>

<h2 id="workaround-2-programmatic-property-application">Workaround 2: Programmatic Property Application</h2>

<p>Alternatively, you can dynamically apply the custom property values directly within your component’s Shadow DOM for more explicit control within the encapsulated scope.</p>

<p>You can use this code to add Tailwind properties to global style sheets:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">styles</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./styles.css?inline</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">shadowSheet</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CSSStyleSheet</span><span class="p">();</span>
<span class="nx">shadowSheet</span><span class="p">.</span><span class="nx">replaceSync</span><span class="p">(</span><span class="nx">styles</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/:root/ug</span><span class="p">,</span> <span class="dl">'</span><span class="s1">:host</span><span class="dl">'</span><span class="p">));</span>

<span class="kd">const</span> <span class="nx">globalSheet</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CSSStyleSheet</span><span class="p">();</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">rule</span> <span class="k">of</span> <span class="nx">shadowSheet</span><span class="p">.</span><span class="nx">cssRules</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">rule</span> <span class="k">instanceof</span> <span class="nx">CSSPropertyRule</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">globalSheet</span><span class="p">.</span><span class="nx">insertRule</span><span class="p">(</span><span class="nx">rule</span><span class="p">.</span><span class="nx">cssText</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="nb">document</span><span class="p">.</span><span class="nx">adoptedStyleSheets</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="nx">globalSheet</span><span class="p">);</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">MyComponent</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">shadowRoot</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">attachShadow</span><span class="p">({</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">open</span><span class="dl">'</span> <span class="p">});</span>
    <span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">adoptedStyleSheets</span> <span class="o">=</span> <span class="p">[</span><span class="nx">shadowSheet</span><span class="p">];</span>
    <span class="c1">// ...</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Or you can replace <code class="language-plaintext highlighter-rouge">@property</code> with variables like this:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">styles</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./styles.css?inline</span><span class="dl">'</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">shadowSheet</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">CSSStyleSheet</span><span class="p">();</span>
<span class="nx">shadowSheet</span><span class="p">.</span><span class="nx">replaceSync</span><span class="p">(</span><span class="nx">styles</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/:root/ug</span><span class="p">,</span> <span class="dl">'</span><span class="s1">:host</span><span class="dl">'</span><span class="p">));</span>

<span class="kd">const</span> <span class="nx">properties</span> <span class="o">=</span> <span class="p">[];</span>
<span class="k">for</span> <span class="p">(</span><span class="kd">const</span> <span class="nx">rule</span> <span class="k">of</span> <span class="nx">shadowSheet</span><span class="p">.</span><span class="nx">cssRules</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">rule</span> <span class="k">instanceof</span> <span class="nx">CSSPropertyRule</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">rule</span><span class="p">.</span><span class="nx">initialValue</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">properties</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">rule</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="s2">: </span><span class="p">${</span><span class="nx">rule</span><span class="p">.</span><span class="nx">initialValue</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
<span class="nx">shadowSheet</span><span class="p">.</span><span class="nx">insertRule</span><span class="p">(</span><span class="s2">`:host { </span><span class="p">${</span><span class="nx">properties</span><span class="p">.</span><span class="nx">join</span><span class="p">(</span><span class="dl">'</span><span class="s1">; </span><span class="dl">'</span><span class="p">)}</span><span class="s2"> }`</span><span class="p">);</span>

<span class="k">export</span> <span class="kd">class</span> <span class="nx">MyComponent</span> <span class="kd">extends</span> <span class="nx">HTMLElement</span> <span class="p">{</span>
  <span class="kd">constructor</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">super</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">shadowRoot</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">attachShadow</span><span class="p">({</span> <span class="na">mode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">open</span><span class="dl">'</span> <span class="p">});</span>
    <span class="nx">shadowRoot</span><span class="p">.</span><span class="nx">adoptedStyleSheets</span> <span class="o">=</span> <span class="p">[</span><span class="nx">shadowSheet</span><span class="p">];</span>
    <span class="c1">// ...</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name>Anton Skshidlevsky</name></author><category term="tailwindcss" /><summary type="html"><![CDATA[Workarounds for using Tailwind CSS v4 with Shadow DOM components due to lack of @property support.]]></summary></entry><entry><title type="html">URI parser in JavaScript</title><link href="https://meefik.github.io/2024/10/20/uri-parser-in-js/" rel="alternate" type="text/html" title="URI parser in JavaScript" /><published>2024-10-20T12:00:00+00:00</published><updated>2024-10-20T12:00:00+00:00</updated><id>https://meefik.github.io/2024/10/20/uri-parser-in-js</id><content type="html" xml:base="https://meefik.github.io/2024/10/20/uri-parser-in-js/"><![CDATA[<p>In this post, I show a lightweight JavaScript approach to parse a connection string URI like MongoDB connection string. The code breaks down the URI into its components, including the scheme, credentials, hosts, endpoint, and options.</p>

<p>Input:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mongodb://user:pass@host1:27017,host2:27017/db?option1=value1&amp;option2=value2
</code></pre></div></div>

<p>Output:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"scheme"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mongodb"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"user"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"password"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pass"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"hosts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"host"</span><span class="p">:</span><span class="w"> </span><span class="s2">"host1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">27017</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"host"</span><span class="p">:</span><span class="w"> </span><span class="s2">"host2"</span><span class="p">,</span><span class="w"> </span><span class="nl">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">27017</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="p">],</span><span class="w">
  </span><span class="nl">"endpoint"</span><span class="p">:</span><span class="w"> </span><span class="s2">"db"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"options"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nl">"option1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"value1"</span><span class="p">,</span><span class="w"> </span><span class="nl">"option2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"value2"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>I like to use simple and useful own code instead of using external models. So I prepared the URI parser code on pure JavaScript, here it is.</p>

<!--more-->

<p>So here is the source code:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
 * Parses URI address.
 *
 * @param {string} addresses The address(es) to process.
 * @returns {Array&lt;Object&gt;} Parsed addresses.
 */</span>
<span class="kd">function</span> <span class="nx">parseAddress</span><span class="p">(</span><span class="nx">addresses</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">addresses</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">,</span><span class="dl">'</span><span class="p">).</span><span class="nx">map</span><span class="p">((</span><span class="nx">address</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">address</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">:</span><span class="dl">'</span><span class="p">);</span>
    <span class="k">return</span> <span class="nx">i</span> <span class="o">&gt;=</span> <span class="mi">0</span>
      <span class="p">?</span> <span class="p">{</span>
          <span class="na">host</span><span class="p">:</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">address</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">i</span><span class="p">)),</span>
          <span class="na">port</span><span class="p">:</span> <span class="o">+</span><span class="nx">address</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">),</span>
        <span class="p">}</span>
      <span class="p">:</span> <span class="p">{</span> <span class="na">host</span><span class="p">:</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">address</span><span class="p">)</span> <span class="p">};</span>
  <span class="p">});</span>
<span class="p">}</span>

<span class="cm">/**
 * Parses URI options.
 *
 * @param {string} options The options to process.
 * @returns {Object} Parsed options.
 */</span>
<span class="kd">function</span> <span class="nx">parseOptions</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="p">{};</span>
  <span class="nx">options</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">&amp;</span><span class="dl">'</span><span class="p">).</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">option</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">i</span> <span class="o">=</span> <span class="nx">option</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">=</span><span class="dl">'</span><span class="p">);</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">i</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="nx">result</span><span class="p">[</span><span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">option</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nx">i</span><span class="p">))]</span> <span class="o">=</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">option</span><span class="p">.</span><span class="nx">substring</span><span class="p">(</span><span class="nx">i</span> <span class="o">+</span> <span class="mi">1</span><span class="p">));</span>
    <span class="p">}</span>
  <span class="p">});</span>
  <span class="k">return</span> <span class="nx">result</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/**
 * Takes a connection string URI of form:
 *
 *   scheme://[username[:password]@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[endpoint]][?options]
 *
 * and returns an object of form:
 *
 *   {
 *     scheme: string,
 *     username?: string,
 *     password?: string,
 *     hosts: [ { host: string, port?: number }, ... ],
 *     endpoint?: string,
 *     options?: object
 *   }
 *
 * @param {string} uri The connection string URI.
 * @returns {Object} Parsed URI object.
 */</span>
<span class="kd">function</span> <span class="nx">parseURI</span><span class="p">(</span><span class="nx">uri</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">uri</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
  <span class="kd">const</span> <span class="nx">connectionStringParser</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span>
    <span class="dl">'</span><span class="s1">^</span><span class="se">\\</span><span class="s1">s*</span><span class="dl">'</span> <span class="o">+</span>                  <span class="c1">// Optional whitespace at the beginning</span>
    <span class="dl">'</span><span class="s1">([^:]+)://</span><span class="dl">'</span> <span class="o">+</span>             <span class="c1">// Scheme (Group 1)</span>
    <span class="dl">'</span><span class="s1">(?:([^:@,/?&amp;]+)(?::([^:@,/?&amp;]+))?@)?</span><span class="dl">'</span> <span class="o">+</span> <span class="c1">// Username (Group 2) and Password (Group 3)</span>
    <span class="dl">'</span><span class="s1">([^@/?&amp;]+)</span><span class="dl">'</span> <span class="o">+</span>             <span class="c1">// Host address(es) (Group 4)</span>
    <span class="dl">'</span><span class="s1">(?:/([^:@,/?&amp;]+)?)?</span><span class="dl">'</span> <span class="o">+</span>     <span class="c1">// Endpoint (Group 5)</span>
    <span class="dl">'</span><span class="s1">(?:</span><span class="se">\\</span><span class="s1">?([^:@,/?]+)?)?</span><span class="dl">'</span> <span class="o">+</span>    <span class="c1">// Options (Group 6)</span>
    <span class="dl">'</span><span class="se">\\</span><span class="s1">s*$</span><span class="dl">'</span><span class="p">,</span>                   <span class="c1">// Optional whitespace at the end</span>
    <span class="dl">'</span><span class="s1">gi</span><span class="dl">'</span>
  <span class="p">);</span>
  <span class="kd">const</span> <span class="nx">connectionStringObject</span> <span class="o">=</span> <span class="p">{};</span>
  <span class="kd">const</span> <span class="nx">tokens</span> <span class="o">=</span> <span class="nx">connectionStringParser</span><span class="p">.</span><span class="nx">exec</span><span class="p">(</span><span class="nx">uri</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="nb">Array</span><span class="p">.</span><span class="nx">isArray</span><span class="p">(</span><span class="nx">tokens</span><span class="p">))</span> <span class="p">{</span>
    <span class="nx">connectionStringObject</span><span class="p">.</span><span class="nx">scheme</span> <span class="o">=</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">1</span><span class="p">];</span>
    <span class="nx">connectionStringObject</span><span class="p">.</span><span class="nx">username</span> <span class="o">=</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span> <span class="p">?</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">tokens</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span> <span class="p">:</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">2</span><span class="p">];</span>
    <span class="nx">connectionStringObject</span><span class="p">.</span><span class="nx">password</span> <span class="o">=</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="p">?</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">tokens</span><span class="p">[</span><span class="mi">3</span><span class="p">])</span> <span class="p">:</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">3</span><span class="p">];</span>
    <span class="nx">connectionStringObject</span><span class="p">.</span><span class="nx">hosts</span> <span class="o">=</span> <span class="nx">parseAddress</span><span class="p">(</span><span class="nx">tokens</span><span class="p">[</span><span class="mi">4</span><span class="p">]);</span>
    <span class="nx">connectionStringObject</span><span class="p">.</span><span class="nx">endpoint</span> <span class="o">=</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">5</span><span class="p">]</span> <span class="p">?</span> <span class="nb">decodeURIComponent</span><span class="p">(</span><span class="nx">tokens</span><span class="p">[</span><span class="mi">5</span><span class="p">])</span> <span class="p">:</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">5</span><span class="p">];</span>
    <span class="nx">connectionStringObject</span><span class="p">.</span><span class="nx">options</span> <span class="o">=</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="p">?</span> <span class="nx">parseOptions</span><span class="p">(</span><span class="nx">tokens</span><span class="p">[</span><span class="mi">6</span><span class="p">])</span> <span class="p">:</span> <span class="nx">tokens</span><span class="p">[</span><span class="mi">6</span><span class="p">];</span>
  <span class="p">}</span>
  <span class="k">return</span> <span class="nx">connectionStringObject</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Use the <code class="language-plaintext highlighter-rouge">parseURI()</code> function to parse URI string. Look at the code example:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// MongoDB connection string as example</span>
<span class="kd">const</span> <span class="nx">uri</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">mongodb://user:pass@host1:27017,host2:27017/db?option1=value1&amp;option2=value2</span><span class="dl">'</span><span class="p">;</span>
<span class="c1">// parse this string</span>
<span class="kd">const</span> <span class="nx">params</span> <span class="o">=</span> <span class="nx">parseURI</span><span class="p">(</span><span class="nx">uri</span><span class="p">);</span>
<span class="c1">// show parsed parameters</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">params</span><span class="p">);</span>
<span class="c1">// {</span>
<span class="c1">//   scheme: 'mongodb',</span>
<span class="c1">//   username: 'user',</span>
<span class="c1">//   password: 'pass',</span>
<span class="c1">//   hosts: [ { host: 'host1', port: 27017 }, { host: 'host2', port: 27017 } ],</span>
<span class="c1">//   endpoint: 'db',</span>
<span class="c1">//   options: { option1: 'value1', option2: 'value2' }</span>
<span class="c1">// }</span>
</code></pre></div></div>

<p>This simple yet powerful parser efficiently breaks down a URI connection string into usable components. It’s especially useful when dealing with multiple hosts or various options in connection strings. Experiment with different URIs to see its in action.</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="javascript" /><summary type="html"><![CDATA[A lightweight JavaScript URI parser that breaks down connection strings into their components.]]></summary></entry><entry><title type="html">Create and verify JWT with pure JavaScript</title><link href="https://meefik.github.io/2024/07/14/jwt-with-pure-js/" rel="alternate" type="text/html" title="Create and verify JWT with pure JavaScript" /><published>2024-07-14T12:00:00+00:00</published><updated>2024-07-14T12:00:00+00:00</updated><id>https://meefik.github.io/2024/07/14/jwt-with-pure-js</id><content type="html" xml:base="https://meefik.github.io/2024/07/14/jwt-with-pure-js/"><![CDATA[<p><a href="https://jwt.io">JSON Web Token</a> (JWT) is a popular standard for securely transmitting information between parties as a JSON object. They are commonly used for authentication and information exchange. While many libraries exist to handle JWTs, sometimes you might need or want to implement the core logic yourself using pure JavaScript, especially in environments like web workers or edge functions where dependencies might be limited.</p>

<p>This post demonstrates how to create and verify JWTs using the <a href="https://developer.mozilla.org/docs/Web/API/Web_Crypto_API">Web Crypto API</a> available in modern browsers and Node.js (v15+). We’ll focus on the HS256 algorithm (HMAC with SHA-256).</p>

<!--more-->

<p>A JWT consists of three parts separated by dots (.):</p>

<ol>
  <li><strong>Header</strong>: Contains metadata about the token, like the signing algorithm (alg) and token type (typ).</li>
  <li><strong>Payload</strong>: Contains the claims (the actual data), such as user ID, name, roles, etc.</li>
  <li><strong>Signature</strong>: Used to verify the sender of the JWT and ensure that the message wasn’t changed along the way.</li>
</ol>

<p>All parts are Base64Url encoded.</p>

<p>First, we need functions to encode/decode Base64Url and to handle HMAC signatures using the Web Crypto API:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Base64Url Encoding/Decoding</span>
<span class="kd">function</span> <span class="nx">encodeBase64Url</span><span class="p">(</span><span class="nx">source</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Encode ArrayBuffer to Base64 string</span>
  <span class="c1">// Then convert Base64 to Base64Url</span>
  <span class="k">return</span> <span class="nx">btoa</span><span class="p">(</span><span class="nb">String</span><span class="p">.</span><span class="nx">fromCharCode</span><span class="p">.</span><span class="nx">apply</span><span class="p">(</span><span class="kc">null</span><span class="p">,</span> <span class="nx">source</span><span class="p">))</span>
    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/=+$/</span><span class="p">,</span> <span class="dl">''</span><span class="p">)</span> <span class="c1">// Remove padding '='</span>
    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">\+</span><span class="sr">/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">-</span><span class="dl">'</span><span class="p">)</span> <span class="c1">// Replace '+' with '-'</span>
    <span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">\/</span><span class="sr">/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">_</span><span class="dl">'</span><span class="p">);</span> <span class="c1">// Replace '/' with '_'</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">decodeBase64Url</span><span class="p">(</span><span class="nx">source</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Convert Base64Url back to Base64</span>
  <span class="nx">source</span> <span class="o">=</span> <span class="nx">source</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/-/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">+</span><span class="dl">'</span><span class="p">).</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/_/g</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/</span><span class="dl">'</span><span class="p">);</span>
  <span class="c1">// Pad with '=' to make it valid Base64</span>
  <span class="nx">source</span> <span class="o">+=</span> <span class="dl">'</span><span class="s1">=</span><span class="dl">'</span><span class="p">.</span><span class="nx">repeat</span><span class="p">((</span><span class="mi">4</span> <span class="o">-</span> <span class="nx">source</span><span class="p">.</span><span class="nx">length</span> <span class="o">%</span> <span class="mi">4</span><span class="p">)</span> <span class="o">%</span> <span class="mi">4</span><span class="p">);</span>
  <span class="c1">// Decode Base64 string to original string</span>
  <span class="k">return</span> <span class="nx">atob</span><span class="p">(</span><span class="nx">source</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// HMAC Signature Generation</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">createHmacSignature</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">message</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">encoder</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">keyData</span> <span class="o">=</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">messageData</span> <span class="o">=</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
  <span class="c1">// Import the secret key for signing</span>
  <span class="kd">const</span> <span class="nx">cryptoKey</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">importKey</span><span class="p">(</span>
    <span class="dl">'</span><span class="s1">raw</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// key format</span>
    <span class="nx">keyData</span><span class="p">,</span> <span class="c1">// key material</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="na">hash</span><span class="p">:</span> <span class="dl">'</span><span class="s1">SHA-256</span><span class="dl">'</span> <span class="p">},</span> <span class="c1">// algorithm details</span>
    <span class="kc">false</span><span class="p">,</span> <span class="c1">// non-exportable</span>
    <span class="p">[</span><span class="dl">'</span><span class="s1">sign</span><span class="dl">'</span><span class="p">]</span> <span class="c1">// key usages</span>
  <span class="p">);</span>
  <span class="c1">// Sign the message</span>
  <span class="kd">const</span> <span class="nx">signature</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">sign</span><span class="p">(</span><span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="nx">cryptoKey</span><span class="p">,</span> <span class="nx">messageData</span><span class="p">);</span>
  <span class="k">return</span> <span class="k">new</span> <span class="nb">Uint8Array</span><span class="p">(</span><span class="nx">signature</span><span class="p">);</span> <span class="c1">// Return signature as Uint8Array</span>
<span class="p">}</span>

<span class="c1">// HMAC Signature Verification</span>
<span class="k">async</span> <span class="kd">function</span> <span class="nx">verifyHmacSignature</span><span class="p">(</span><span class="nx">key</span><span class="p">,</span> <span class="nx">message</span><span class="p">,</span> <span class="nx">signature</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">encoder</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">keyData</span> <span class="o">=</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">key</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">messageData</span> <span class="o">=</span> <span class="nx">encoder</span><span class="p">.</span><span class="nx">encode</span><span class="p">(</span><span class="nx">message</span><span class="p">);</span>
  <span class="c1">// Decode Base64Url signature to Uint8Array</span>
  <span class="kd">const</span> <span class="nx">signatureData</span> <span class="o">=</span> <span class="nb">Uint8Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="nx">decodeBase64Url</span><span class="p">(</span><span class="nx">signature</span><span class="p">),</span> <span class="nx">c</span> <span class="o">=&gt;</span> <span class="nx">c</span><span class="p">.</span><span class="nx">charCodeAt</span><span class="p">(</span><span class="mi">0</span><span class="p">));</span>
  <span class="c1">// Import the secret key for verification</span>
  <span class="kd">const</span> <span class="nx">cryptoKey</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">importKey</span><span class="p">(</span>
    <span class="dl">'</span><span class="s1">raw</span><span class="dl">'</span><span class="p">,</span>
    <span class="nx">keyData</span><span class="p">,</span>
    <span class="p">{</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="na">hash</span><span class="p">:</span> <span class="dl">'</span><span class="s1">SHA-256</span><span class="dl">'</span> <span class="p">},</span>
    <span class="kc">false</span><span class="p">,</span>
    <span class="p">[</span><span class="dl">'</span><span class="s1">verify</span><span class="dl">'</span><span class="p">]</span> <span class="c1">// key usage</span>
  <span class="p">);</span>
  <span class="c1">// Verify the signature</span>
  <span class="k">return</span> <span class="nx">crypto</span><span class="p">.</span><span class="nx">subtle</span><span class="p">.</span><span class="nx">verify</span><span class="p">(</span><span class="dl">'</span><span class="s1">HMAC</span><span class="dl">'</span><span class="p">,</span> <span class="nx">cryptoKey</span><span class="p">,</span> <span class="nx">signatureData</span><span class="p">,</span> <span class="nx">messageData</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, let’s combine these to create and verify JWT:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">createJWT</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="nx">secret</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Define the header</span>
  <span class="kd">const</span> <span class="nx">header</span> <span class="o">=</span> <span class="p">{</span> <span class="na">alg</span><span class="p">:</span> <span class="dl">'</span><span class="s1">HS256</span><span class="dl">'</span><span class="p">,</span> <span class="na">typ</span><span class="p">:</span> <span class="dl">'</span><span class="s1">JWT</span><span class="dl">'</span> <span class="p">};</span>
  <span class="c1">// Encode header and payload</span>
  <span class="kd">const</span> <span class="nx">encodedHeader</span> <span class="o">=</span> <span class="nx">encodeBase64Url</span><span class="p">(</span><span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">header</span><span class="p">)));</span>
  <span class="kd">const</span> <span class="nx">encodedPayload</span> <span class="o">=</span> <span class="nx">encodeBase64Url</span><span class="p">(</span><span class="k">new</span> <span class="nx">TextEncoder</span><span class="p">().</span><span class="nx">encode</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">payload</span><span class="p">)));</span>
  <span class="c1">// Create the data to sign (header + '.' + payload)</span>
  <span class="kd">const</span> <span class="nx">dataToSign</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">encodedHeader</span><span class="p">}</span><span class="s2">.</span><span class="p">${</span><span class="nx">encodedPayload</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="c1">// Create the signature</span>
  <span class="kd">const</span> <span class="nx">signature</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">createHmacSignature</span><span class="p">(</span><span class="nx">secret</span><span class="p">,</span> <span class="nx">dataToSign</span><span class="p">);</span>
  <span class="c1">// Encode the signature</span>
  <span class="kd">const</span> <span class="nx">encodedSignature</span> <span class="o">=</span> <span class="nx">encodeBase64Url</span><span class="p">(</span><span class="nx">signature</span><span class="p">);</span>
  <span class="c1">// Combine all parts into the JWT</span>
  <span class="k">return</span> <span class="s2">`</span><span class="p">${</span><span class="nx">dataToSign</span><span class="p">}</span><span class="s2">.</span><span class="p">${</span><span class="nx">encodedSignature</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">async</span> <span class="kd">function</span> <span class="nx">verifyJWT</span><span class="p">(</span><span class="nx">token</span><span class="p">,</span> <span class="nx">secret</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// Split the token into its parts</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">header</span><span class="p">,</span> <span class="nx">payload</span><span class="p">,</span> <span class="nx">signature</span><span class="p">]</span> <span class="o">=</span> <span class="nx">token</span><span class="p">.</span><span class="nx">split</span><span class="p">(</span><span class="dl">'</span><span class="s1">.</span><span class="dl">'</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">header</span> <span class="o">||</span> <span class="o">!</span><span class="nx">payload</span> <span class="o">||</span> <span class="o">!</span><span class="nx">signature</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Invalid JWT format</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// Decode and parse the header</span>
  <span class="kd">const</span> <span class="nx">decodedHeader</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">decodeBase64Url</span><span class="p">(</span><span class="nx">header</span><span class="p">));</span>
  <span class="c1">// Basic header validation</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">decodedHeader</span><span class="p">?.</span><span class="nx">alg</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">HS256</span><span class="dl">'</span> <span class="o">||</span> <span class="nx">decodedHeader</span><span class="p">?.</span><span class="nx">typ</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">JWT</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Invalid JWT header</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// Prepare the data that was originally signed</span>
  <span class="kd">const</span> <span class="nx">dataToVerify</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">header</span><span class="p">}</span><span class="s2">.</span><span class="p">${</span><span class="nx">payload</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
  <span class="c1">// Verify the signature</span>
  <span class="kd">const</span> <span class="nx">isValid</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">verifyHmacSignature</span><span class="p">(</span><span class="nx">secret</span><span class="p">,</span> <span class="nx">dataToVerify</span><span class="p">,</span> <span class="nx">signature</span><span class="p">);</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="nx">isValid</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Invalid signature</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// Check if the payload has expired</span>
  <span class="kd">const</span> <span class="nx">decodedPayload</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">decodeBase64Url</span><span class="p">(</span><span class="nx">payload</span><span class="p">));</span>
  <span class="k">if</span> <span class="p">(</span><span class="nx">decodedPayload</span><span class="p">.</span><span class="nx">exp</span> <span class="o">&amp;&amp;</span> <span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">&gt;=</span> <span class="nx">decodedPayload</span><span class="p">.</span><span class="nx">exp</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Token has expired</span><span class="dl">'</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="c1">// If signature is valid, return the decoded payload</span>
  <span class="k">return</span> <span class="nx">decodedPayload</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>That’s it. Now we can use it, see the example:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// Define the secret key used for both signing and verification</span>
  <span class="kd">const</span> <span class="nx">secret</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">your-256-bit-secret</span><span class="dl">'</span><span class="p">;</span>
  
  <span class="c1">// Define a payload with user information. This data will be embedded in the JWT.</span>
  <span class="kd">const</span> <span class="nx">payload</span> <span class="o">=</span> <span class="p">{</span> <span class="na">exp</span><span class="p">:</span> <span class="o">~~</span><span class="p">(</span><span class="nb">Date</span><span class="p">.</span><span class="nx">now</span><span class="p">()</span> <span class="o">/</span> <span class="mi">1000</span><span class="p">)</span> <span class="o">+</span> <span class="mi">60</span><span class="p">,</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">John Doe</span><span class="dl">'</span> <span class="p">};</span>

  <span class="c1">// Create a JWT using the payload and the secret key</span>
  <span class="kd">const</span> <span class="nx">token</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">createJWT</span><span class="p">(</span><span class="nx">payload</span><span class="p">,</span> <span class="nx">secret</span><span class="p">);</span>
  
  <span class="c1">// Log the generated JWT to the console</span>
  <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Generated JWT:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">token</span><span class="p">);</span>
  
  <span class="k">try</span> <span class="p">{</span>
    <span class="c1">// Verify the JWT using the same secret key to ensure its validity</span>
    <span class="kd">const</span> <span class="nx">decodedPayload</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">verifyJWT</span><span class="p">(</span><span class="nx">token</span><span class="p">,</span> <span class="nx">secret</span><span class="p">);</span>
    
    <span class="c1">// Log the decoded payload if the verification succeeds</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Decoded payload:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">decodedPayload</span><span class="p">);</span>
  <span class="p">}</span> <span class="k">catch</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// Log an error message if verification fails</span>
    <span class="nx">console</span><span class="p">.</span><span class="nx">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Verification failed:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">})();</span>
</code></pre></div></div>

<p>While libraries abstract away much of this complexity, understanding the underlying mechanics of JWT creation and verification using standard browser APIs can be valuable.</p>]]></content><author><name>Anton Skshidlevsky</name></author><category term="javascript" /><category term="jwt" /><summary type="html"><![CDATA[Implementing JSON Web Token (JWT) creation and verification using pure JavaScript and the Web Crypto API.]]></summary></entry></feed>