DEV Community

PicklePixel
PicklePixel

Posted on

How I Made Netflix Give Me 4K (Because Apparently My Browser Wasn't Good Enough)

I pay for Netflix Premium. The one that's supposed to include 4K streaming. But every time I tried to watch something on my PC, it would cap at 1080p. Sometimes even 720p. On a 4K monitor. With gigabit internet.

This had been bugging me for months. I'd sit down to watch something, notice the quality looked soft, check the stats overlay, and sure enough 1080p. Every single time. I figured it was a bandwidth thing at first, maybe Netflix's servers were busy. But it kept happening.

So I finally decided to figure out what was actually going on.

The Discovery

Turns out Netflix has a list of "approved" devices and browsers for 4K playback. If you're not on Microsoft Edge, Safari, or their native app, you simply don't get 4K. Doesn't matter that you're paying for it. Doesn't matter that your hardware supports it.

Chrome? No 4K. Firefox? No 4K. Brave? Nope.

The reasoning has to do with DRM. Netflix requires hardware-level content protection (Widevine L1 and HDCP 2.2) to serve 4K streams. Edge on Windows has this. Chrome doesn't - it only has Widevine L3, which is software-based and considered less secure by content providers.

I get the security argument from Netflix's perspective. But from my perspective, I'm paying for a service tier I can't fully use because of my browser choice. That felt worth fixing.

Down the Rabbit Hole

I spent an evening just researching how Netflix determines device capabilities. Opened DevTools, watched the network requests, read through forums and old GitHub issues. The picture that emerged was interesting.

Before Netflix serves you any video, it runs a bunch of capability checks:

Browser fingerprinting:

  • User agent string (what browser/OS you're running)
  • Screen resolution via window.screen
  • Device pixel ratio

Codec support:

  • HEVC (H.265) for 4K
  • VP9 as an alternative
  • AV1 on newer content
  • Dolby Vision for HDR

DRM capabilities:

  • Widevine security level (L1, L2, or L3)
  • PlayReady support on Windows
  • HDCP version compliance

Media APIs:

  • navigator.mediaCapabilities.decodingInfo()
  • MediaSource.isTypeSupported()
  • navigator.requestMediaKeySystemAccess()

All of these checks happen in JavaScript before the video manifest is even requested. Netflix builds a profile of what your device can handle, then serves you the appropriate stream quality.

The key insight: most of these checks are JavaScript APIs that can be intercepted and spoofed.

Day One: Basic Spoofing

I started with the obvious stuff. Created a basic Chrome extension and began overriding the simple checks:

// Spoof screen resolution to 4K
Object.defineProperty(window.screen, 'width', { get: () => 3840 });
Object.defineProperty(window.screen, 'height', { get: () => 2160 });
Object.defineProperty(window.screen, 'availWidth', { get: () => 3840 });
Object.defineProperty(window.screen, 'availHeight', { get: () => 2160 });
Object.defineProperty(window.screen, 'colorDepth', { get: () => 48 });
Object.defineProperty(window, 'devicePixelRatio', { get: () => 1 });
Enter fullscreen mode Exit fullscreen mode

Then the user agent. Netflix needs to think we're running Edge:

Object.defineProperty(navigator, 'userAgent', {
  get: () => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0'
});
Enter fullscreen mode Exit fullscreen mode

Loaded it up, went to Netflix, played a video. Still 1080p. The basic spoofs weren't enough.

Day Two: Media Capabilities

The next layer was the Media Capabilities API. Netflix uses this to ask the browser "can you smoothly decode this codec at this resolution?"

I intercepted the decodingInfo method and forced it to return positive results for 4K codecs:

const originalDecodingInfo = navigator.mediaCapabilities.decodingInfo.bind(navigator.mediaCapabilities);

navigator.mediaCapabilities.decodingInfo = async function(config) {
  const dominated4KCodecs = ['hev1', 'hvc1', 'vp09', 'vp9', 'av01', 'dvhe', 'dvh1'];

  if (config.video) {
    const codec = config.video.contentType || '';
    const dominated = dominated4KCodecs.some(c => codec.toLowerCase().includes(c));

    if (dominated || config.video.width >= 3840) {
      return {
        supported: true,
        smooth: true,
        powerEfficient: true
      };
    }
  }

  return originalDecodingInfo(config);
};
Enter fullscreen mode Exit fullscreen mode

Same idea for MediaSource.isTypeSupported():

const originalIsTypeSupported = MediaSource.isTypeSupported.bind(MediaSource);

MediaSource.isTypeSupported = function(mimeType) {
  const dominated4KTypes = ['hev1', 'hvc1', 'dvh1', 'dvhe', 'vp09', 'vp9', 'av01'];

  if (dominated4KTypes.some(t => mimeType.toLowerCase().includes(t))) {
    return true;
  }

  return originalIsTypeSupported(mimeType);
};
Enter fullscreen mode Exit fullscreen mode

Tested again. Still 1080p. Netflix was checking something else.

Day Three: DRM and HDCP

This is where it got interesting. Netflix checks DRM capabilities through navigator.requestMediaKeySystemAccess(). This API negotiates which DRM system to use (Widevine, PlayReady) and what security level.

The security level is specified through a "robustness" string. For 4K, Netflix wants HW_SECURE_ALL (hardware-level security). Chrome can only provide SW_SECURE_DECODE (software).

I tried intercepting this and requesting the higher robustness level:

navigator.requestMediaKeySystemAccess = async function(keySystem, configs) {
  const enhancedConfigs = configs.map(config => {
    const enhanced = JSON.parse(JSON.stringify(config));
    if (enhanced.videoCapabilities) {
      enhanced.videoCapabilities = enhanced.videoCapabilities.map(vc => ({
        ...vc,
        robustness: 'HW_SECURE_ALL'
      }));
    }
    return enhanced;
  });

  try {
    return await originalRequestMediaKeySystemAccess(keySystem, enhancedConfigs);
  } catch (e) {
    // Fall back to original if enhanced fails
    return originalRequestMediaKeySystemAccess(keySystem, configs);
  }
};
Enter fullscreen mode Exit fullscreen mode

The enhanced request fails (because Chrome genuinely doesn't have L1), but the fallback still works. The interesting part is that this interception, combined with all the other spoofs, started to have an effect.

I also spoofed the HDCP policy check:

Object.defineProperty(navigator, 'hdcpPolicyCheck', {
  value: () => Promise.resolve({ hdcp: 'hdcp-2.2' })
});
Enter fullscreen mode Exit fullscreen mode

Day Four: Cadmium

Netflix's video player is called Cadmium. It's their internal player that handles everything from manifest fetching to adaptive bitrate switching. Cadmium has its own configuration that sets maximum resolution and bitrate caps.

Finding how to hook into it took some time. I ended up polling for the window.netflix.player object and overriding its methods:

const hookCadmium = () => {
  if (window.netflix && window.netflix.player) {
    const player = window.netflix.player;

    ['create', 'configure', 'getConfiguration', 'getConfig'].forEach(method => {
      if (typeof player[method] === 'function') {
        const original = player[method].bind(player);
        player[method] = function(...args) {
          const result = original(...args);

          if (result && typeof result === 'object') {
            if (result.maxBitrate !== undefined) result.maxBitrate = 16000;
            if (result.maxVideoHeight !== undefined) result.maxVideoHeight = 2160;
            if (result.maxVideoWidth !== undefined) result.maxVideoWidth = 3840;
          }

          return result;
        };
      }
    });
  }
};
Enter fullscreen mode Exit fullscreen mode

I also intercepted Object.defineProperty itself to catch any resolution or bitrate caps being set anywhere in Netflix's code:

const originalDefineProperty = Object.defineProperty;
Object.defineProperty = function(obj, prop, descriptor) {
  if (typeof prop === 'string') {
    const lowerProp = prop.toLowerCase();

    if (lowerProp.includes('maxbitrate') && descriptor.value < 16000) {
      descriptor.value = 16000;
    }
    if (lowerProp.includes('maxheight') && descriptor.value < 2160) {
      descriptor.value = 2160;
    }
  }

  return originalDefineProperty.call(this, obj, prop, descriptor);
};
Enter fullscreen mode Exit fullscreen mode

This felt hacky, but it was catching config values that I couldn't find any other way.

The Breakthrough

After all these layers of spoofing, I loaded Netflix, played a 4K title (Our Planet - nature docs are great for testing because the quality difference is obvious), and pressed Ctrl+Shift+Alt+D to bring up the stats overlay.

3840x2160. 15000+ kbps bitrate.

It actually worked.

The quality difference was immediately visible. All that fine detail in the nature footage that looked muddy at 1080p was now crisp. This is what I was paying for.

The SPA Problem

Feeling good about it, I browsed around Netflix, clicked on another movie, and... 1080p again.

Netflix is a single-page application. When you click on a title, it doesn't do a full page reload - it just updates the URL and swaps content via JavaScript. My extension was injecting at page load, but the Cadmium player was being recreated for each new video without triggering a reload.

I added navigation detection that watches for URL changes:

let lastPath = location.pathname;

const checkNavigation = () => {
  if (location.pathname !== lastPath) {
    const isWatch = location.pathname.startsWith('/watch');
    lastPath = location.pathname;

    if (isWatch) {
      // Reset and re-hook
      cadmiumHooked = false;
      setTimeout(() => hookCadmium(), 1000);
    }
  }
};

setInterval(checkNavigation, 300);

// Also intercept history API
const originalPushState = history.pushState;
history.pushState = function(...args) {
  originalPushState.apply(this, args);
  setTimeout(checkNavigation, 100);
};
Enter fullscreen mode Exit fullscreen mode

This helped, but honestly it's still not perfect. Sometimes the timing is off and the hooks don't catch the new player instance. When that happens, a page refresh fixes it. I've been trying to nail down the exact race condition but haven't fully solved it yet. It works maybe 80% of the time on navigation, and a refresh always fixes it when it doesn't.

Good enough for now. I'll revisit it when it annoys me enough.

What I Learned

The whole thing took about four days of evening tinkering. Most of that was research and understanding how Netflix's capability detection works. The actual code isn't that complicated once you know what to intercept.

The layers matter. Netflix doesn't rely on any single check. You have to spoof the user agent AND the screen resolution AND the media capabilities AND the DRM negotiation AND the player config. Miss one layer and you're back to 1080p.

Hardware DRM is the real barrier. All my JavaScript spoofing can't change the fact that Chrome has Widevine L3 and not L1. I'm essentially tricking Netflix into trying to serve 4K, and it works because the actual decryption still happens (just at a lower security level than Netflix prefers). This might not work for all content, and Netflix could theoretically detect the mismatch and block it.

Edge is the sweet spot. If you use this extension on Microsoft Edge (which does have Widevine L1), you get the best of both worlds - real hardware DRM plus all the spoofed capability checks. That's probably the most reliable setup.

The Repo

I published the extension if anyone else wants it: netflix-force-4k

Fair warning: Netflix could patch this whenever they want, and the SPA navigation still needs a refresh sometimes. But it works well enough that I'm actually getting 4K on most of what I watch now.

The fact that I had to reverse-engineer a streaming service to access the quality tier I'm paying for is a little absurd. But that's the state of DRM in 2025. You pay for the content, but whether you can actually watch it in full quality depends on which browser you prefer.

Anyway, that's the journey. Four days of API hooking to watch movies in higher resolution. Worth it.

Top comments (0)