<?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: Louis Truong</title>
    <description>The latest articles on DEV Community by Louis Truong (@louis_truong).</description>
    <link>https://dev.to/louis_truong</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4188395%2F10a0b555-e3de-4588-8c56-ad6abac1fdca.jpg</url>
      <title>DEV Community: Louis Truong</title>
      <link>https://dev.to/louis_truong</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/louis_truong"/>
    <language>en</language>
    <item>
      <title>I couldn't change the quality of Facebook Reels, so I built a Chrome extension that can</title>
      <dc:creator>Louis Truong</dc:creator>
      <pubDate>Fri, 16 Oct 2026 09:06:24 +0000</pubDate>
      <link>https://dev.to/louis_truong/i-couldnt-change-the-quality-of-facebook-reels-so-i-built-a-chrome-extension-that-can-3nb8</link>
      <guid>https://dev.to/louis_truong/i-couldnt-change-the-quality-of-facebook-reels-so-i-built-a-chrome-extension-that-can-3nb8</guid>
      <description>&lt;p&gt;Lately I've been hooked on AI-made short films. The Vietnamese channels that translate them keep getting copyright strikes on YouTube, so I started watching them on Facebook instead. Then a new annoyance appeared: halfway through a reel, Facebook would quietly drop the quality to 540p, then 360p. On a fast connection. And Reels has no quality setting at all.&lt;/p&gt;

&lt;p&gt;So I built one. &lt;strong&gt;Reel Quality&lt;/strong&gt; is a small Chrome extension that adds a ⚙ button to the reel you're watching, lists the resolutions that reel actually has, and lets you pick one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxyso0cgw6l9pwdvbdj0y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxyso0cgw6l9pwdvbdj0y.png" alt="Quality menu on a reel" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/nbgnfkagjipmlfhlpilandmenjmilmcc" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/nbgnfkagjipmlfhlpilandmenjmilmcc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source (MIT): &lt;a href="https://github.com/trieuluan/reel-quality-for-facebook" rel="noopener noreferrer"&gt;https://github.com/trieuluan/reel-quality-for-facebook&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Transparency note:&lt;/strong&gt; I built this with a lot of help from an AI coding assistant (Claude Code), and it also helped me edit this post. The debugging on real Facebook pages, the design decisions and the bugs are mine.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's how it works under the hood, because it turned out to be more interesting than I expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Where do the qualities come from?
&lt;/h2&gt;

&lt;p&gt;Facebook plays Reels with &lt;strong&gt;DASH over Media Source Extensions&lt;/strong&gt;. The &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; element only has a &lt;code&gt;blob:&lt;/code&gt; URL, so there's nothing to swap. But the page receives a DASH manifest for every reel, embedded in its GraphQL responses as a &lt;code&gt;dash_manifest&lt;/code&gt; string (an MPD XML document). Each &lt;code&gt;&amp;lt;Representation&amp;gt;&lt;/code&gt; in it is one quality, with a &lt;code&gt;FBQualityLabel&lt;/code&gt; like &lt;code&gt;720p&lt;/code&gt;, its own &lt;code&gt;BaseURL&lt;/code&gt;, and byte ranges for the init segment and the segment index.&lt;/p&gt;

&lt;p&gt;The extension runs a content script in the page's &lt;strong&gt;MAIN world&lt;/strong&gt; at &lt;code&gt;document_start&lt;/code&gt;, wraps &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;XMLHttpRequest&lt;/code&gt;, and scans JSON responses for &lt;code&gt;dash_manifest&lt;/code&gt;. The first reel's data is server-rendered into &lt;code&gt;&amp;lt;script type="application/json"&amp;gt;&lt;/code&gt; tags, so those get scanned too.&lt;/p&gt;

&lt;p&gt;One fun surprise: facebook.com enforces &lt;strong&gt;Trusted Types&lt;/strong&gt;, so &lt;code&gt;DOMParser&lt;/code&gt; is off the table. The MPD is simple enough that a few regular expressions parse it reliably.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Which manifest belongs to which video?
&lt;/h2&gt;

&lt;p&gt;The page holds manifests for several reels at once, so I needed to tie each one to its &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt;. The chain I ended up with:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Facebook's player downloads a segment whose URL path matches a &lt;code&gt;BaseURL&lt;/code&gt; from some manifest. I remember the response's byte length.&lt;/li&gt;
&lt;li&gt;The player appends a buffer of that same length to a &lt;code&gt;SourceBuffer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;That &lt;code&gt;SourceBuffer&lt;/code&gt; belongs to a &lt;code&gt;MediaSource&lt;/code&gt;, which was turned into the &lt;code&gt;blob:&lt;/code&gt; URL that sits in &lt;code&gt;video.src&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appendBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SourceBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendBuffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;SourceBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appendBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;recentSizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;byteLength&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;ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sbToMs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;msToEntry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;appendBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hooks on &lt;code&gt;URL.createObjectURL&lt;/code&gt; and &lt;code&gt;MediaSource.prototype.addSourceBuffer&lt;/code&gt; fill in the other two links. If the chain ever fails, the &lt;code&gt;/reel/&amp;lt;id&amp;gt;&lt;/code&gt; in the address bar is a decent fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Facebook won't switch quality, so don't ask it to
&lt;/h2&gt;

&lt;p&gt;This was the key decision. Facebook's player reads the manifest once and picks a quality on its own, and there's no public way to tell it otherwise. Rewriting its internals felt fragile.&lt;/p&gt;

&lt;p&gt;Instead, the extension lays &lt;strong&gt;its own &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; over Facebook's&lt;/strong&gt; and plays the chosen quality there, video only. Facebook's player keeps running underneath and still owns the sound, the controls, looping and seeking. My layer just follows its clock:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;drift&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;MAX_DRIFT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;            &lt;span class="c1"&gt;// big jump: seek&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playbackRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;drift&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// small drift: nudge the speed&lt;/span&gt;
  &lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playbackRate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playbackRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playbackRate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// Only show the HD layer when it is actually in sync; otherwise Facebook's picture shows through.&lt;/span&gt;
&lt;span class="nx"&gt;me&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inSync&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last line is what makes it feel seamless: you never get a black frame while the HD layer is buffering, you just see Facebook's own picture for a moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A tiny DASH player
&lt;/h2&gt;

&lt;p&gt;Each Facebook representation is a single fragmented MP4 with a &lt;code&gt;sidx&lt;/code&gt; box (segment index). Read the &lt;code&gt;sidx&lt;/code&gt; and you know exactly which byte range holds which seconds of video:&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;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&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;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="mh"&gt;0x7fffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// referenced_size&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// subsegment_duration&lt;/span&gt;
  &lt;span class="nx"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;timescale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;timescale&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;byte&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;time&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there it's a small loop: fetch the init segment, then keep about 20 seconds buffered ahead of Facebook's current time with range requests, and trim what's behind.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Bonus: landscape clips stuck in vertical reels
&lt;/h2&gt;

&lt;p&gt;A lot of reels are really a horizontal clip pasted into a 9:16 frame, with black bars and a caption on top. Version 1.1 spots those and can show the clip large in the middle of the screen.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9fmqwiky6gktv99bsihi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9fmqwiky6gktv99bsihi.png" alt="Expand landscape clip" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My first version looked at one frame and searched for bright rows. It failed badly on a night-time clip, where most of the "clip" was as black as the bars. The fix was to gather evidence over several frames instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bars&lt;/strong&gt; stay black and never change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Captions&lt;/strong&gt; in the bars are bright but rarely change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The clip&lt;/strong&gt; changes almost all the time, even when it's dark.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So each pixel tracks its maximum brightness and how often it changes. Rows that move are clip rows. Then the band snaps to a common aspect ratio (16:9, 1.85, 2.39…); if it's shorter than 16:9, part of a dark clip was probably mistaken for bars, so it falls back to a centered 16:9 window. Finally, everything outside the band has to look like bars, or it's just a normal vertical reel and the option stays hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;While a fixed quality is selected, Facebook still streams its own copy underneath, so bandwidth goes up by roughly 25–30%. On Auto, the extension does nothing at all.&lt;/li&gt;
&lt;li&gt;Facebook changes its internals often. When it does, this will break until I fix it.&lt;/li&gt;
&lt;li&gt;Desktop Chromium browsers only. Mobile browsers don't run extensions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/nbgnfkagjipmlfhlpilandmenjmilmcc" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/nbgnfkagjipmlfhlpilandmenjmilmcc&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/trieuluan/reel-quality-for-facebook" rel="noopener noreferrer"&gt;https://github.com/trieuluan/reel-quality-for-facebook&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It collects no data and asks for no extra permissions. If you find a reel where it doesn't work, an issue on GitHub would help a lot. And I'd love to hear how you'd have approached the "can't control the player" problem differently.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
