DEV Community

Super Funicular
Super Funicular

Posted on

Recording With the Screen Off, Streaming Over LAN: The Hard Parts of Building a Phone-as-Camera App on Android in 2026

There is a whole class of Android app that sounds trivial until you actually try to build it: take a phone, point it at something, and let the owner watch and record from somewhere else — with the screen off, without an account, and without shipping a single frame to anybody's cloud. "It's just the camera plus a socket," you think. Then you meet Doze mode, MediaProjection consent dialogs you didn't ask for, a camera pipeline that assumes there's a live preview surface, and an OS that would very much like to kill your process the moment the display sleeps.

I build Background Camera RemoteStream, an app that does exactly this — turns a spare Android into a screen-off camera you can view over your own Wi-Fi or an unlisted stream, with recordings kept on the device. This post isn't a feature pitch; it's a tour of the four engineering problems that make this harder than it looks, and the architectural choices that a privacy-first, no-cloud design forces on you. If you've shipped anything against Camera2, foreground services, or embedded servers on Android, most of this will be familiar terrain with a few sharp edges I want to point at directly.

Problem 1: The camera doesn't want to run without a screen

The intuitive mental model — "open the camera, keep it open" — collides immediately with how Android manages both the camera stack and process lifecycle. Two separate things fight you here, and it's worth keeping them distinct because the fixes are different.

The first is the camera pipeline itself. Camera2 (and CameraX on top of it) is built around output surfaces: you hand the capture session a set of Surface targets — a preview TextureView, an ImageReader, a MediaRecorder input — and frames flow to them. The naive tutorials all wire a preview surface into the session, which quietly assumes a visible UI. For a screen-off design you have to build the capture session around targets that don't need the display: a MediaRecorder/MediaCodec surface for the file, and an ImageReader or a second encoder surface for whatever you're going to stream. Once you stop treating the on-screen preview as mandatory, the camera is perfectly happy to keep delivering frames to an encoder with the display fully asleep. The preview was never the point; it was just the most convenient consumer of frames in a demo.

The second fight is the OS trying to reclaim your process. This is where a foreground service with the camera (and, if you're recording audio, microphone) foreground-service type is not optional — it's the contract you sign with the system that says "this work is user-visible and ongoing, don't reap it." On modern Android that means declaring the typed foreground service in the manifest, posting an ongoing notification the user can see, and starting the service from an allowed context. Get any part of that wrong on Android 14+ and you'll meet ForegroundServiceStartNotAllowedException or a MissingForegroundServiceTypeException, usually in a crash report from exactly one OEM.

Even with a legitimate foreground service, Doze and app-standby will throttle timers, jobs, and network the moment the device is stationary and the screen is off — which is the default state for a camera that's supposed to sit in a window for days. The lesson that took the longest to internalize: don't lean on postDelayed, Handler heartbeats, or periodic jobs to keep the pipeline alive. Keep the work on a continuously-running encoder driven by the camera's own frame callbacks, hold the minimal wake lock the recording genuinely needs, and stop assuming the scheduler will wake you politely. The screen-off case isn't an edge case you harden for later; it's the primary case, and everything else is the exception. (I've written separately about the specifics of keeping a session alive with the display fully off — linked at the bottom.)

Problem 2: Streaming without becoming spyware — the consent surface

Here's a design fork that turns out to be a values decision disguised as an API choice. There are two broad ways to get "what the camera sees" off the device and onto another screen:

  1. The camera pipeline — you're the app that owns the camera, so you encode the frames you're already capturing.
  2. MediaProjection / screen capture — you grab the whole screen's framebuffer.

It is genuinely tempting to reach for screen capture for the "view from anywhere" feature, because it's a well-trodden path. But MediaProjection captures the entire display, and — by deliberate platform design — it forces a system consent dialog every time a capture session starts, precisely so that no app can silently record a user's screen. That dialog exists because screen-streaming is exactly the capability malware abuses. (This isn't hypothetical; a recurring pattern in Android RAT families is finding ways to reach screen-streaming capability around that consent surface. The dialog is the immune system.)

For a camera app the right answer is the boring one: stay in the camera pipeline. You already have the frames legitimately, from a sensor the user pointed on purpose, under a foreground service they can see in their notification shade. You never touch MediaProjection, you never capture the screen, and there's simply no framebuffer of the user's private apps to leak. The architectural constraint — "only ever encode camera frames, never the screen" — is also the privacy guarantee. When the mechanism and the promise are the same line of code, you can't accidentally betray one while keeping the other.

Problem 3: A web server that lives inside the app

The feature people underestimate most is "open a web address on your laptop and see the camera, with nothing leaving the house." The clean way to do that is to run an actual HTTP server inside the Android process. On Kotlin, an embedded Ktor server (Netty/CIO engine) does this well: you bind to the device's LAN address on a chosen port, and serve a tiny web UI plus the video stream straight from the app.

The interesting engineering isn't standing up the server — it's the streaming transport over a link you don't control, to a browser you didn't write, with no cloud in the middle to smooth things over. A few things that bit me:

  • MJPEG is unglamorous and it works. A multipart/x-mixed-replace stream of JPEG frames plays in any browser with zero client code, no WebRTC signaling, no codec negotiation, no plugin. The latency and bandwidth are worse than a real video codec, but on a same-room LAN it's fine, and "works in every browser instantly" beats "elegant but needs a signaling server" — especially when a signaling server would mean a cloud dependency, which is the one thing the whole design refuses to have. For lower-latency or higher-resolution cases you move to a proper MediaCodec H.264 stream, but the MJPEG path is what makes "just open the link" true on day one.
  • You are now a server on someone's home network, so you treat it like one: bind deliberately, keep the surface tiny, don't assume the LAN is friendly. "Local-only" is a real security posture, but only if you actually hold the line at the network boundary instead of waving at it.
  • Backpressure is your job. A browser tab on a slow link will fall behind the camera's frame rate. Without a drop policy you buffer frames until something dies. The fix is to always serve the newest frame and discard stale ones — a viewer wants "now," not a growing backlog of "a few seconds ago." Newest-frame-wins is the correct default for a live view, and it's a decision you have to make on purpose because the naive queue does the opposite.

The reward for keeping the server in-process and LAN-first is that the common case — you're home, on your own network — involves no third party at all. The frames go phone → your router → your laptop. Nobody's servers, nobody's retention policy, nobody's outage.

Problem 4: "From anywhere" without a cloud you built

LAN-only is a clean story right up until the user leaves the house. The honest tension: "view from anywhere" usually means "I run infrastructure, I hold your video, I bill you monthly." That's the exact business model a privacy-first app is trying not to be.

The escape hatch is to lean on a streaming endpoint the user already trusts and that you don't operate: an unlisted YouTube Live stream. Architecturally the app becomes an RTMP publisher — you authenticate the user's own Google account, create or bind a broadcast + stream via the YouTube Live API, pull the ingest URL and stream key, and push your encoded H.264/AAC up that RTMP endpoint. The viewer just opens an unlisted link. Crucially, you never store the video, never see the account beyond the OAuth scope needed to start the broadcast, and never stand up a media server. The reach-me-anywhere feature exists without a Super Funicular data center existing, which is the entire point.

It has honest costs, and I'd rather name them than let the marketing imply otherwise. A live stream has the few-seconds latency every live stream has — it is not a real-time video call. It depends on the user's Google account and YouTube's availability. And it's a genuinely different privacy model from the LAN path: the LAN view touches nobody, the YouTube path routes your video through Google under an unlisted link. Presenting those as two clearly labeled options — same-network web view vs. from-anywhere unlisted stream — instead of one blurred "cloud" feature is itself a design decision, and the more truthful one.

What the architecture is really optimizing for

Step back and the four problems share a spine. Screen-off recording forces you to treat the display as optional and the foreground service as load-bearing. Streaming forces you to stay in the camera pipeline and never touch screen capture. The web view forces you to run a real server and hold the line at the LAN boundary. "From anywhere" forces you to borrow infrastructure you don't own rather than build a cloud. In every case the privacy-first, no-cloud constraint didn't just shape the marketing — it removed options from the design, and mostly the options it removed were the ones that would have quietly made the user the product.

That's the part I find genuinely interesting as an engineer: "we don't run a cloud" reads like a limitation, but downstream it's a forcing function that keeps the mechanism and the promise identical. There's no server to leak because there's no server. There's no screen to capture because you only ever encode the sensor. The recording is on the device because the device is the only place it was ever written. You don't have to trust those properties; they fall out of the architecture.

If you're building anything in this space — home camera, field monitor, a livestream rig out of a phone headed for the landfill — the four sharp edges above are where I'd spend your first week. And if you'd rather just use the thing, Background Camera RemoteStream is free on Google Play, with no account, no subscription, and recordings kept on the device: play.google.com/store/apps/details?id=com.superfunicular.digicam. More on the design philosophy — and what it deliberately doesn't do — at superfunicular.com.


More reading:

Top comments (0)