You set up an old Android phone as a camera. It records with the screen off, and it runs a small web server so you can open the picture in a browser on your laptop. You type http://192.168.1.42:8080, the feed comes up, and everything works.
Then you notice three things that don't.
The page cannot keep your laptop screen awake while you watch. It cannot register a service worker, so it re-fetches its own shell every single time. And if you try to embed that feed in any dashboard you host over HTTPS, the frame stays empty — not slow, not broken, empty.
None of these are bugs in the server. They are not bugs in the browser either. They are all the same decision, made once, in a specification that never mentions your phone, your network, or private IP addresses at all.
I went looking for the sentence that causes it. The interesting part turned out to be a sentence that isn't there.
The line itself
The W3C Secure Contexts specification defines whether an origin is "potentially trustworthy." Browsers use that one boolean to gate a large set of web APIs. The algorithm is short. In order, an origin is trustworthy if:
- Its scheme is
httpsorwss. - Its host matches the CIDR notations
127.0.0.0/8or::1/128. - Its host is
localhost,localhost., or ends in.localhost/.localhost.— but only "If the user agent conforms to the name resolution rules in [let-localhost-be-localhost]." - Its scheme is
file(a SHOULD, not a MUST). - Its scheme is one the user agent considers authenticated, such as
chrome-extension:. - It "has been configured as a trustworthy origin."
Otherwise: Not Trustworthy.
Two details in that list are worth slowing down for, because most summaries get them wrong.
The loopback CIDR is unconditional. 127.0.0.1 is trustworthy, full stop. The name localhost is not — it depends on the user agent following particular name-resolution rules, and §5.2 softens it further to a MAY. People say "localhost is a secure context" as if the two were the same rule. They aren't.
And the spec is explicit that the usual levers don't apply: "Neither origin's domain nor port has any effect on whether or not it is considered to be a secure context." Moving your server from 8080 to 8443 changes nothing. It was never about the port.
The sentence that isn't there
Here is the part I actually went to check, because I expected to find a carve-out and an argument about why it was narrow.
There is no carve-out. There is no argument.
I searched the Secure Contexts document for 192.168, 10.0.0.0, RFC1918, "private IP", "private network", "intranet", and 169.254. Every one of them returns zero occurrences. The only CIDR notations in the entire specification are 127.0.0.0/8 and ::1/128.
Private network addresses are not excluded by a rule. They simply fall off the end of the list into step 8, Not Trustworthy, alongside every plain-HTTP address on the public internet. The spec does not discuss them, does not note them as a known gap, and does not leave an issue open about them.
The one remaining route is §7.2, and it is a manual one:
"In order to support developers who run staging servers on non-loopback hosts, the user agent MAY allow users to configure specific sets of origins as trustworthy"
That is a per-user, per-browser, per-machine setting intended for development. It is not something a device on your network can earn, negotiate, or opt into. Every person who ever opens the page would have to set it themselves, in their own browser.
So there is a line, and it runs in a strange place. I've started calling it the loopback line: the boundary between a server talking to itself and the same server talking to the machine next to it.
A phone that serves its camera page to itself is on the trusted side. The instant that identical page — same code, same server, same process, same device — is served to a laptop one hop away, it lands on the untrusted side. Nothing about the software changed. The only thing that changed is that the bytes left the device.
Which is, of course, the entire point of a camera.
What lands on the untrusted side of the line
"Not a secure context" is abstract until you list what it costs. MDN maintains the list of features restricted to secure contexts. For a LAN camera page, these are the ones that bite:
Service workers. Gone. The Secure Contexts spec is blunt: "Only secure contexts may register them." No offline shell, no cached UI, no graceful behaviour when the phone briefly drops off Wi-Fi. Every load is a cold load.
Notifications. Gone. The viewer page cannot raise a desktop notification, which rules out a whole category of "tell me when the feed drops" built in the page itself.
crypto.subtle. Gone — and here is a precision worth keeping, because it's easy to overclaim. It is crypto.subtle that requires a secure context. crypto.getRandomValues() and crypto.randomUUID() do not. "Web Crypto needs HTTPS" is too broad a statement; the subtle-crypto interface is the part that's gated.
getUserMedia(). Gone. MDN lists it separately from the main table, under methods that "require a secure context (even if the associated API does not)." So the LAN page cannot open a camera of its own — relevant the moment you imagine two-way anything.
Screen Wake Lock. Gone. And this is the one I keep turning over.
The Screen Wake Lock API is how a web page asks the browser not to let the display sleep. It exists almost entirely for pages you watch without touching: recipes, dashboards, sheet music, live feeds. It is precisely, exactly the API a camera viewer wants.
A LAN camera page cannot have it. The page whose entire job is to stay visible is denied the one API about staying visible.
There's a symmetry here that I find genuinely funny, in a bleak way. On the recording end, the thing that makes screen-off capture work at all is an Android partial wake lock held by a foreground service — keep the CPU running, let the display die. I've written about that machinery in the context of how Chrome treats a camera tab you walk away from. The recorder fights to keep the CPU awake while the screen sleeps. The viewer wants to keep the screen awake — and is told it hasn't earned the right to ask.
Two wake locks, opposite goals, and only one of them is available to you.
Mixed content, and the trap in the middle of it
The second symptom — the empty iframe — is a different mechanism with the same root.
If you serve any page over HTTPS and it tries to pull a subresource over plain HTTP, that's mixed content. Current browser behaviour splits it into upgradable and blockable content. Scripts, fetch(), XMLHttpRequest, stylesheets, fonts, <object>, sendBeacon and <iframe> sources are all blockable — they fail, they are not retried over HTTP.
Images, audio and video are the upgradable category. The browser rewrites the request to HTTPS and tries again. Which sounds like a loophole: serve an MJPEG snapshot as an <img> and let it upgrade.
It doesn't work, and the reason is a carve-out that's easy to miss. From MDN's mixed content page:
"Mixed content requests that would otherwise be upgraded are blocked if the URL's host is an IP address rather than a domain name."
An upgrade to HTTPS only makes sense if there's a name that could plausibly have a certificate. 192.168.1.42 is an IP literal, so <img src="http://192.168.1.42/snapshot.jpg"> on an HTTPS page is blocked, not upgraded. The category that was supposed to be the lenient one is, for your camera specifically, the strict one.
One more piece of precision, since this is the kind of thing that gets repeated wrongly for years. The "upgradable/blockable" vocabulary is from Mixed Content Level 2, which states that "Upgradeable content was previously referred as optionally-blockable." Level 2 is a First Public Working Draft from October 2020. The CR-level published document is still Level 1, which uses the older "optionally-blockable" wording. If you cite this, cite the level.
And a confirmed absence, because I looked: Mixed Content Level 2 contains zero occurrences of the word "iframe." It reaches iframes through its algorithm — it permits a request whose "destination is 'document', and request's target browsing context has no parent browsing context", i.e. top-level navigations only. A framed document falls through to blocked. MDN says "iframes are blocked" in plain words; the spec never does.
What changed in 2026, and why it doesn't rescue you
For a few years the expected fix for all of this was Private Network Access — a scheme where a public page wanting to reach a private address would send a CORS preflight carrying Access-Control-Request-Private-Network: true, and the local device would answer with Access-Control-Allow-Private-Network: true.
Two corrections, because I nearly wrote this section as current behaviour and it isn't.
First, PNA is a WICG Draft Community Group Report. In its own words: "It is not a W3C Standard nor is it on the W3C Standards Track."
Second, and more importantly: Chrome does not enforce it. From Chrome's own PNA on hold post: "PNA preflights are not currently enforced," the Chrome 130 rollout having been halted over compatibility problems. Anything you read describing that preflight as live browser behaviour is out of date.
What replaced it is Local Network Access, a permission prompt rather than a preflight — "Local Network Access replaces that effort, after PNA was put on hold."
Now look at the shape of it, because the shape is the whole story. LNA is a permission that a page asks for in order to reach into your local network. And "the ability to request this permission is restricted to secure contexts."
So the direction of travel is: an HTTPS page, already trusted, may ask your permission to reach your camera. Your camera's own page, serving from your own network, gains nothing. It is still not a secure context, still cannot register a service worker, still cannot hold a screen wake lock.
The platform is building a door for traffic coming in from the public web. It is not building one for a device on your LAN that wants to be trusted by the machine next to it. If anything, the asymmetry is widening: the capability now flows toward origins that already have certificates.
(This is the browser's half of the story. The operating system has its own, separate story about apps reaching LAN devices — different mechanism, different gate, and not what any of the above describes.)
So why not just serve HTTPS?
Because the certificate model and a phone on your living-room Wi-Fi were designed for different worlds, and a public CA will not issue a certificate for a private IP address. I've written that argument out properly in the LAN threat model for a phone-as-camera server, including why a self-signed certificate trades one problem for a worse habit, so I won't re-run it here.
The relevant point for this article is narrower: even if you solved the transport question, you would be solving it to recover browser APIs, not to protect an already-local link. That's a strange thing to spend a user's trust on.
The design consequence
Once you accept that the viewer page lives permanently on the untrusted side of the loopback line, a lot of architectural decisions stop being decisions.
You build the console as a self-contained page with no secure-context dependencies. No service worker, so no offline shell — the page has to be small enough that a cold load is cheap. No wake lock, so if a user wants the screen to stay on while they watch, that's a setting on their own device, and the honest thing is to say so rather than to ship a button that silently fails. No notifications from the page, so status has to be visible in the page.
And the transport has to be something a plain, unprivileged browser context does natively and well: direct HTTP, an MJPEG or similar stream for live view, and ordinary byte-range requests for recorded files. That last one is less boring than it sounds — the Range header does most of the work of seeking in a recording, which is exactly the kind of capability that survives on the untrusted side because it was never gated in the first place. The embedded-server side of this, on Ktor, I've taken apart separately.
The result looks austere next to a cloud camera's web app. That austerity isn't a shortcut. It's the shape of what remains after you remove everything a non-secure context isn't allowed to touch.
This is the architecture behind Background Camera RemoteStream — an Android app that records with the screen off and serves its own feed over your LAN from a built-in web server, with no cloud account in the path. The design constraints above are not incidental to that; they're most of why the viewer looks the way it does.
The honest trade
A vendor camera with a cloud dashboard has none of these problems, and it's worth being clear about why: their dashboard is served from a domain they own, over HTTPS, with a certificate they renew. It's a secure context. It gets service workers, notifications, wake lock, the lot. That's a real advantage and there's no point pretending otherwise.
What it costs is that the picture goes to their server so that their page can be trusted by your browser. The trust is real, and it's rented.
A phone on your own network keeps the picture where it is and pays for it in browser capabilities — a page that must be simple, stateless between loads, and unable to ask your laptop for much of anything.
I don't think that's a bad trade. But I'd rather explain it as a trade than let someone discover it as a series of small unexplained failures, which is how almost everyone meets the loopback line for the first time.
Background Camera RemoteStream — record with the screen off, stream to YouTube Live, view remotely over your LAN from a built-in web server, with local-only storage.
- Google Play: https://play.google.com/store/apps/details?id=com.superfunicular.digicam
- Site: https://superfunicular.com
Specification text quoted above was read from the linked primary sources: W3C Secure Contexts, W3C Mixed Content Level 2, the WICG Private Network Access draft, MDN, and Chrome's developer blog. Where a document is a draft rather than a standard, I've said so inline, because the status turned out to matter more than once.
Top comments (0)