<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Thibaut Lion</title>
    <description>The latest articles on DEV Community by Thibaut Lion (@privaloops).</description>
    <link>https://dev.to/privaloops</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3890302%2F37a3b893-6dbc-4bde-8010-96c1c02d004d.jpeg</url>
      <title>DEV Community: Thibaut Lion</title>
      <link>https://dev.to/privaloops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/privaloops"/>
    <language>en</language>
    <item>
      <title>Playing HEVC in a Browser Without Plugin — An H.265 Decoder in WebAssembly</title>
      <dc:creator>Thibaut Lion</dc:creator>
      <pubDate>Tue, 21 Apr 2026 08:00:42 +0000</pubDate>
      <link>https://dev.to/privaloops/playing-hevc-in-a-browser-without-plugin-an-h265-decoder-in-webassembly-4ag0</link>
      <guid>https://dev.to/privaloops/playing-hevc-in-a-browser-without-plugin-an-h265-decoder-in-webassembly-4ag0</guid>
      <description>&lt;h2&gt;
  
  
  The Problem — HEVC Everywhere Except the Browser
&lt;/h2&gt;

&lt;p&gt;HEVC/H.265 is the standard codec for Netflix, Apple, broadcasters, 4K/HDR. It saves 30-50% bandwidth versus H.264 at equivalent quality — millions in annual CDN savings for streaming services.&lt;/p&gt;

&lt;p&gt;But browser support is a mess.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS&lt;/strong&gt; — Safari, Chrome, Edge, Firefox all decode HEVC natively via VideoToolbox. No extension needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chrome 107+ on Windows&lt;/strong&gt; — uses D3D11VA directly. No Microsoft extension required, but needs a GPU with hardware HEVC decoder (Intel Skylake 2015+, NVIDIA Maxwell 2nd gen+, AMD Fiji+). No software fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Edge on Windows&lt;/strong&gt; — uses Media Foundation. &lt;strong&gt;Requires&lt;/strong&gt; the Microsoft &lt;a href="https://apps.microsoft.com/detail/9nmzlz57r3t7" rel="noopener noreferrer"&gt;HEVC Video Extension&lt;/a&gt; ($1 on the Store). Without it, no HEVC regardless of GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firefox 133+ on Windows&lt;/strong&gt; — same MFT path, same extension dependency.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linux&lt;/strong&gt; — Chrome with VAAPI, maybe. Firefox, no.&lt;/p&gt;

&lt;p&gt;The root cause is licensing. MPEG LA and Access Advance impose per-unit royalties. Microsoft passes this to users via the Store extension. Google negotiated a direct D3D11VA path. Mozilla relies on Microsoft's extension. The result: publishers must either encode everything twice (H.264 + HEVC) or accept that some users get a black screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution — Decode HEVC Client-Side in WebAssembly
&lt;/h2&gt;

&lt;p&gt;What if the browser didn't need to know it's playing HEVC?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/privaloops/hevc.js" rel="noopener noreferrer"&gt;hevc.js&lt;/a&gt; decodes HEVC in a Web Worker and re-encodes to H.264 via WebCodecs, delivering standard H.264 to Media Source Extensions. The player doesn't know it's happening.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fMP4 HEVC → mp4box.js (demux) → NAL units
         → WASM H.265 decoder → YUV frames
         → WebCodecs VideoEncoder → H.264
         → custom fMP4 muxer → MSE → &amp;lt;video&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The HEVC decoder is a from-scratch C++17 implementation of ITU-T H.265 (716 pages), compiled to WebAssembly. 236 KB gzipped. Zero dependencies. No special server headers needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  dash.js integration
&lt;/h3&gt;

&lt;p&gt;The plugin intercepts &lt;code&gt;MediaSource.addSourceBuffer()&lt;/code&gt;. When dash.js creates an HEVC SourceBuffer, a proxy accepts the HEVC MIME type but feeds the real SourceBuffer with H.264. ABR, seek, live — everything works unmodified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;dashjs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;attachHevcSupport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@hevcjs/dashjs-plugin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dashjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;MediaPlayer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;attachHevcSupport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;workerUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/transcode-worker.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;wasmUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/hevc-decode.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;videoElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mpdUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Smart detection
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;MediaSource.isTypeSupported()&lt;/code&gt; can lie — Firefox on Windows reports HEVC support even without the Video Extension installed. hevc.js actually creates a SourceBuffer to probe; only activates transcoding on failure. When native HEVC works: zero overhead, WASM never loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser Compatibility
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Browser + OS&lt;/th&gt;
&lt;th&gt;Native HEVC&lt;/th&gt;
&lt;th&gt;hevc.js activates?&lt;/th&gt;
&lt;th&gt;Transcoding?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Safari 13+ (macOS/iOS)&lt;/td&gt;
&lt;td&gt;Yes (VideoToolbox)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome/Edge/Firefox (Mac)&lt;/td&gt;
&lt;td&gt;Yes (VideoToolbox)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome 107+ (Win, HEVC GPU)&lt;/td&gt;
&lt;td&gt;Yes (D3D11VA)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome 107+ (Win, no HEVC GPU)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge (Win, with extension)&lt;/td&gt;
&lt;td&gt;Yes (MFT)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edge (Win, no extension)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox 133+ (Win, with extension)&lt;/td&gt;
&lt;td&gt;Yes (MFT)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firefox 133+ (Win, no extension)&lt;/td&gt;
&lt;td&gt;False positive&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome/Edge 94-106&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chrome (Linux, no VAAPI)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Requirements: WebAssembly, Web Workers, Secure Context (HTTPS), WebCodecs with H.264 encoding support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;Single-threaded, Apple Silicon:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Native C++&lt;/th&gt;
&lt;th&gt;WASM (Chrome)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1080p decode&lt;/td&gt;
&lt;td&gt;76 fps&lt;/td&gt;
&lt;td&gt;61 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4K decode&lt;/td&gt;
&lt;td&gt;28 fps&lt;/td&gt;
&lt;td&gt;21 fps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1080p transcode&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;~2.5x realtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;WASM reaches 80% of native C++ speed, and 83% of libde265 (a mature 10-year-old HEVC decoder) when both are compiled to WASM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conformance&lt;/strong&gt;: 128/128 test bitstreams pixel-perfect against ffmpeg. Zero drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tradeoff
&lt;/h2&gt;

&lt;p&gt;The first segment takes 2-3 seconds to transcode — that's the startup latency cost of software decode versus native hardware. After buffering, playback is smooth.&lt;/p&gt;

&lt;p&gt;This makes hevc.js a good fit for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Streaming platforms with existing HEVC catalogs&lt;/li&gt;
&lt;li&gt;Infrastructure simplification (single HEVC pipeline, no H.264 fallback)&lt;/li&gt;
&lt;li&gt;VOD or moderate-latency live&lt;/li&gt;
&lt;li&gt;Controlled environments (IPTV, B2B)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not ideal for: low-end mobile (CPU/battery), 4K on underpowered machines, or ultra-low-latency live sports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://hevcjs.dev/demo/dash.html" rel="noopener noreferrer"&gt;hevcjs.dev/demo/dash.html&lt;/a&gt; — toggle "Force transcoding" to test the WASM path even if your browser has native HEVC.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @hevcjs/dashjs-plugin dashjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/privaloops/hevc.js" rel="noopener noreferrer"&gt;github.com/privaloops/hevc.js&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;MIT license. Feedback and contributions welcome.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>javascript</category>
      <category>video</category>
      <category>streaming</category>
    </item>
  </channel>
</rss>
