← All writing

How I Built a Facebook Video & Reels Browser Extension

How I built a Facebook video player extension using JavaScript, MPEG-DASH, Picture-in-Picture, expiring stream URL recovery, and in-browser MP4 muxing.

What if I could keep watching a Facebook Reel while coding in another tab?

That question started as a small annoyance and eventually became Video Player & Reels Controls for Facebook™, a browser extension I built for Chrome and Firefox. What I expected to be a simple playback-control project led me into adaptive video streaming, browser lifecycle management, expiring media URLs, and MP4 container internals.

The extension is now available on the Chrome Web Store and Firefox Add-ons. This article is the story of why I built it, how the major pieces work, the engineering problems I encountered, and what I learned.

It started with an everyday frustration

Sometimes I find an interesting Facebook Reel, tutorial, podcast, or longer video while browsing on my phone. When I have other things to do, I send the link to my PC so I can continue watching or listening while working.

That sounds like a straightforward workflow. In practice, I wanted more flexibility: to jump backward without fighting the controls, adjust speed or quality, return to the News Feed without losing the video, and keep playback visible while working in another tab.

I tried existing video-player and download extensions, but I couldn't find one that brought together the experience I wanted. Rather than keep switching between tools, I decided to build my own.

My first goal was modest: a cleaner video player with better controls. Then I started asking more questions. Could a mini-player follow me between tabs? Could it use a higher-quality stream when one was available? Why did some downloads contain video but no sound? Every answer revealed another part of the browser's media stack.

What the extension does

The extension combines several related video-watching features in one player:

  • HD playback and quality selection: progressive MP4 and adaptive MPEG-DASH playback, with automatic or manual quality choices when streams are available.
  • Multitasking: a persistent mini-player for supported Facebook navigation and Picture-in-Picture for watching outside the page.
  • Playback customization: speed adjustment, seeking, keyboard shortcuts, subtitles when available, a volume boost, and A–B section looping.
  • Reels navigation: next/previous controls, supported keyboard and scroll interactions, and prefetching intended to smooth transitions.
  • Split-screen comments: video beside Facebook's interactive comments where the page supports it.
  • Supported MP4 downloads: combining separate video and audio tracks locally, without re-encoding, when compatible streams are available.
  • Resume and history: locally stored player preferences, playback positions, and a manageable viewing history.

Features depend on the source video, browser capabilities, and Facebook's changing page and media formats. The extension does not grant access to private or otherwise unavailable content.

The rest of this article focuses on the engineering behind these features rather than documenting every UI control.

Architecture: content scripts, background logic, and the popup

I built the extension primarily with vanilla JavaScript and browser extension APIs. The Chrome version uses Manifest V3. Three main components divide the work:

  1. Content scripts operate on supported Facebook pages. They detect relevant navigation, associate page data with a video, and create the custom player interface.
  2. Background logic handles extension messages, supported media-resolution requests, context-menu actions, and certain download operations. In Chrome, it runs as an extension service worker.
  3. The popup exposes preferences, a URL launcher, and locally stored watch history.

Chrome and Firefox have separate manifests and different background configurations. Sharing application logic is useful, but it does not remove the need to test each browser's supported APIs and playback behavior.

The Manifest V3 lifecycle problem

A Chrome extension service worker is not a permanently running background process. Chrome can suspend it when idle and start it again for events. That means a critical playback operation shouldn't assume state held only in a service worker's global variables will survive indefinitely.

The broader lesson was to keep the components' responsibilities clear and use appropriate browser storage or message-driven state for information that must survive component restarts. Chrome's extension service worker lifecycle documentation is a helpful reference for this constraint.

Challenge 1: identifying the correct video on a changing page

Facebook behaves like a single-page application (SPA). Clicking between the News Feed, Watch, profiles, and Reels can change the current route and content without reloading the whole tab. An extension that looks for a video only once at page load will eventually miss something.

I separated route recognition from player initialization. The route parser identifies supported video and Reel URLs, while the content-side logic reacts to navigation and attempts to resolve the media belonging to the active selection.

The difficult part isn't always finding a media URL. It's finding the right media URL.

A Facebook page can contain metadata for several videos at the same time: the current Reel, a previous Reel, suggested videos, or neighboring feed items. Simply taking the first playable URL from a response could display or download the wrong video. My extraction logic therefore tries to match media information to the active video's identifier, while allowing fallbacks for less predictable page structures.

There's also an asynchronous race: imagine choosing Reel B while a request for Reel A is still running. If A finishes last, it could overwrite B's player state. The code uses a route-sequence value to reject stale refresh responses rather than blindly applying whichever request completes most recently.

Neither approach makes Facebook's internal format stable. It makes failures easier to contain and gives the project a better starting point when the site changes.

Challenge 2: progressive MP4 versus adaptive MPEG-DASH

At first glance, browser video playback looks simple: find a URL and assign it to a <video> element. That works when the source is an ordinary progressive MP4 file.

Higher-quality media can be delivered differently. With MPEG-DASH (Dynamic Adaptive Streaming over HTTP), an MPD manifest describes available representations, such as different video resolutions and audio tracks. The player uses that information to fetch and play compatible media segments.

I ended up supporting two paths:

  • Progressive MP4: play an available direct file URL using the browser's video element.
  • Adaptive DASH: load the locally bundled Shaka Player engine when adaptive playback is needed.

Loading the heavier DASH engine only when necessary keeps ordinary video playback from always paying its initialization cost. The quality menu can expose automatic selection and available fixed resolutions. When a DASH path fails, the player can attempt a compatible MP4 fallback if one exists.

A useful design lesson: a robust fallback is often more valuable than a promise like “always 1080p.” The actual resolution depends on the available stream, browser support, and the video itself.

Challenge 3: making Picture-in-Picture useful for multitasking

The feature I wanted most was the ability to keep watching a Reel while working in another tab.

There are two distinct experiences in the extension:

  • In-site mini-player: keeps the video accessible while moving through supported Facebook pages.
  • Picture-in-Picture (PiP): lets the browser show playback in a floating window beyond the normal page layout.

In browsers supporting the Document Picture-in-Picture API, the extension can place a richer custom player interface in a floating window—not just the video element. Where Document PiP isn't supported or can't be opened, the implementation attempts native video PiP instead.

That distinction matters for cross-browser expectations. The richer floating controls and native Picture-in-Picture are not identical experiences. Availability depends on the browser and version.

I learned to treat these as progressively enhanced paths rather than assume every browser offers the same floating-window capabilities. Browser permissions, navigation, window lifecycle, and user interaction requirements all affect what can remain open.

Challenge 4: refreshing media URLs before they expire

One of the more surprising bugs appeared when a video worked at first but stopped after being paused or left open for a while.

Some media URLs in the project include an oe parameter that the code interprets as an expiry timestamp. A URL that was valid when the video started might no longer be usable by the time playback resumes.

The refresh logic is designed to:

  1. Detect recognizable expiry information on relevant media URLs.
  2. Plan a refresh before the earliest relevant expiry.
  3. Recheck on playback, seeking, or visibility changes, because browsers may delay background timers.
  4. Apply updated URLs to an active DASH session when feasible, or carefully switch a progressive source.
  5. Limit retries and surface an error when recovery fails.

This reduces some avoidable interruptions, but it's not a guarantee. Videos can be removed, access conditions can change, and not every URL or failed request can be recovered.

The bigger insight was that stream discovery is not always a one-time operation. For long-lived playback, validity can become part of ongoing player state.

Challenge 5: why HD downloads sometimes have no audio

This became one of the most technically interesting parts of the entire project.

A progressive MP4 may already include video and audio together. With DASH, high-resolution video and audio can arrive as separate fragmented tracks. Saving only the video stream can leave the user with a silent file, even though the video played with sound in the browser.

For compatible streams, I implemented a client-side ISO-BMFF/MP4 muxing path in JavaScript. At a high level, it:

  1. Selects an available video representation and a compatible audio representation when one exists.
  2. Fetches the media tracks required for that selection.
  3. Parses the MP4 container structures needed to understand fragments, samples, tracks, and timing.
  4. Builds a combined MP4 container, preserving track timing where supported.
  5. Saves the result without re-encoding the video or audio.

Muxing is not transcoding

This distinction shaped the implementation:

  • Muxing combines or reorganizes already-encoded media tracks inside a container.
  • Transcoding decodes and re-encodes media, which is usually much more computationally expensive and can affect quality.

Avoiding re-encoding preserves the original encoded streams and avoids an additional encoding step. It does not mean every codec combination, stream format, or playback application is guaranteed to work.

The memory limitation I had to account for

The current implementation can write large output files through a browser-supported file picker where available. But it still retrieves input tracks into memory before the final write and applies per-track limits. A large recording can therefore consume significant memory or fail.

That's an important distinction: writing the final file to disk is not the same as constant-memory streaming. I prefer to document that limitation rather than describe the feature as handling unlimited file sizes.

The project includes regression tests covering parsing, stream-expiry behavior, and muxing. A separate validation path can use FFmpeg and ffprobe when they're available. Tests with generated fixtures help prevent regressions, but they aren't a substitute for testing real media across browsers and players.

The smaller features also needed careful engineering

Once the core playback paths were in place, many seemingly minor interactions became useful additions: 10-second seeking, adjustable speed, Web Audio-based volume boosting, subtitle rendering, A–B looping, screenshots, keyboard shortcuts, and Reel navigation.

These are closely related to the original goal. They make the player more practical for tutorials, longer videos, and people who want to keep their hands on the keyboard while working.

But each feature has edge cases: controls can be constrained in a narrow vertical Reel, shortcuts can conflict with browser behavior, captions may be absent, and mini-player modes may expose different capabilities. These details are why I consider real-browser testing part of the feature—not something to do only after the UI looks finished.

Privacy, permissions, and the limits of local processing

A video extension needs permission to interact with supported pages and media. This project requests access to Facebook-related pages and media hosts, alongside browser permissions for settings storage, downloads, scripting, network request rules, and context menus.

I aimed to keep player settings and viewing history in browser storage, without a developer-operated analytics or tracking server. However, “processed locally” does not mean the extension never makes network requests. It communicates with Facebook and its media delivery services to resolve and play content, and some requests can happen during prefetching.

Local history, playback resume records, and temporary player handoff data also have different retention rules. Rather than oversimplify them, I document those behaviors in the public privacy policy.

For a browser extension, explaining permissions and data use clearly is part of the product experience, not just a store-submission checkbox.

What building this extension taught me

This project started with a personal workflow problem, but the biggest lessons came from the systems underneath the visible player:

  • Identity matters: the right video is more important than the first playable video.
  • Asynchronous work must respect navigation: stale results shouldn't overwrite newer user choices.
  • Streaming media is stateful: quality changes and expiring URLs require recovery paths.
  • Browser APIs need fallbacks: PiP capabilities differ, and background processes can be suspended.
  • Media containers matter: combining DASH audio and video involves timestamps and MP4 structures, not simply joining two downloads.
  • Honest constraints build trust: large-file limitations and differences between browsers should be explained rather than hidden.

Most importantly, I was reminded that a useful side project doesn't need to begin with a revolutionary idea. Sometimes a recurring annoyance is enough to lead you into unfamiliar APIs, difficult bugs, and a product other people may find useful.

From building to publishing

Building the player was one part of the journey. For the store-submission challenges, including browser-specific manifests, privacy disclosures, and permission reviews, read what it took to publish the extension on Chrome and Firefox.

Try the extension or follow the project

Video Player & Reels Controls for Facebook™ is available for Chrome and Firefox:

The support repository is available for issues, feedback, and privacy information. You can also visit the project case study for a shorter overview.

If you've ever wanted better control over Facebook videos or Reels while working, I'd love to hear whether this extension helps—and which part of the experience you would improve next.

Disclaimer: Facebook™ is a trademark of Meta Platforms, Inc. This is an independent project and is not affiliated with, endorsed by, or sponsored by Meta. Download or use media only when you have the necessary rights or permission.