惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

DEV Community

Fixing the session timeouts Beyond Autonomous AI: Understanding Self-Healing Agents in Enterprise AI Systems MCP Is the AI Platform I built a free Bitly/TinyURL alternative and self-hosted it on a $6/mo VPS — here's the full stack Design to Code #7: How CVA Scaffolding Turned Into Dead Code Stop rebuilding memory and orchestration for every AI agent you build 6 users in one day with zero marketing budget — what actually worked How a photo-blind dating engine actually ranks people (the TypeScript) AI Is Moving From Your Pocket to Your Brain — The 6-Year Timeline I Built a Static Blog Generator in 350 Lines of Python — No Dependencies, No Config, No Nonsense How Does Duolingo Monetize? I Decompiled the Android App (v6.79.5) Next.js Dynamic OG Images: Fix the Turbopack CPU Hang AI Is Turning Every Developer Into an Architect What is props 3 Things Building MediTrack Taught Me About Laravel Vibe Coding: My Daily Workflow with Claude Code Using Python to Do the Wonders: How Flet Changes the Game for Developers OpenDev: From Zero Clients to Linux Independence – How I'm Building a One-Man Linux Revolution Migrating from Jest to Vitest 4: A Complete 2026 Guide Making Equation (2.2) of the OpenAI Erdős Result Executable HTTP request headers: canonical reference Prefix caching in vLLM under multi-tenant agent traffic Introducing Oracle Support in Dory How I built 3 products solo as a CA student using AI — no coding background What is AEO? How to Get ChatGPT, Perplexity & AI Search Engines to Cite Your Website — 2026 Guide HTTP rate-control headers: canonical reference Im attending Manifest 2026! AI Music Doesn’t Need Better Prompts — It Needs Better Systems ORA-00215 오류 원인과 해결 방법 완벽 가이드 Stop Making Your AI Chatbot Slower: Streaming Responses with Spring AI and Server-Sent Events Annotations in Spring Boot What is the Model Context Protocol (MCP)? Gemini CLI Skills: Teaching Your Terminal Agent How to Think 🧠 What the Heck is an API? FairLens AI: An Intelligent Dashboard for Automated Bias Auditing RAG vs Fine-Tuning- Choosing Right Strategy for Modern AI Applications AI Metrics Decoded: From Parameters to TOPS I made git merge finish itself — in VS Code, in my terminal, and in CI You just can’t miss this… Redis Essentials: Architecture, Caching, and Setup Docker with AI: A Practical Guide to Running LLMs, Agents and MCP Design to Code #5: Using AI to Build a Design System Analyzing 1,000 Engineering Problems Through GitHub Data Open Graph protocol: canonical reference How a 400-Engineer SaaS Company Cut PR-to-Production from 4.2 Days to 6.4 Hours with Claude Code Multi-Agent DevOps 💬 Embedded AI Chatbots vs Popup Bubbles — Which One Creates Better Engagement? Bajándole todos los minutos posibles al CI del backend con mas de 1000 tests Harness Engineering: Stop Re-Prompting Your Coding Agent Every Session HTML meta referrer: canonical reference AWS MCP Server Just Gave AI Agents Your Cloud Keys — Here's Why That Should Worry You Announcing the Trust Identity Protocol (TIP): HTTPS for the AI Era We built the feature in two days. Making it reliable took two weeks. LuisCore /for-agents.json — agent bootstrap — daily syndication · 2026-05-26 A Curious Journey Into Reverse Engineering an AI-Generated Python .exe Part 2: Enterprise Decision Intelligence Architecture: AI Governance, Threshold Policy Engines, and Operational AI Systems I will continue using Devise with Rails 8! The Developer's Guide to Picking the Right AI Code Model in 2026 (I Spent $500 So You Don’t Have To) 30 Kubernetes Tasks Every CKA Candidate Should Practice Before Exam Day Why Some Websites Feel Instantly Better to Use Advanced React Patterns I Wish I Knew 5 Years Ago ¿Cómo optimizar algoritmos en arreglos y listas con la técnica de dos punteros? I scanned 8 popular open source repos with one command. Here's what I found. mcp-probe v1.6.0: Stricter GitHub Actions checks for MCP CI gates How we connect two strangers' webcams fast (and keep the TURN bill small) LLM Agents Are Now Finding Zero-Days: How AI is Autonomously Rewriting the Rules of Vulnerability Research Minimal Code Doesn’t Mean Stable Code How I manage 40+ skills across Claude Code, Codex, and .agents folders Hardening Stealth Browser Fingerprint Integrity and State Persistence Quick Tip: Benchmarking Multimodal APIs in Under 10 Minutes How I Slashed My AI API Bill by 92% in 2026 — A Cost Optimizer's Speed Benchmark Guide How I Slashed My AI API Bill by 95% — A Practical Guide for 2026 A Go outbox library that runs inside your own DB transaction How I Built a Credit Optimizer That Saves 30-75% on AI Agent Costs (Open Architecture) The Missing POP: How I Ported a Yul Contract to Huff by Reading Every Opcode The Moment the Config Parser Became the Bottleneck Churn Tool Stack by Revenue Stage ($5K to $50K+) What I Learned Exploring AI-Generated 3D: A Hands-On Tour of Meshy, Tripo, and Three.js Day 15 - Software Composition Analysis(SCA) Contributing Upstream Instead of Forking: My grape-swagger-rails Story Behind The Badge: How We Built 2,000 Hackable Badges For Temporal Replay Access Control Doesn't Scale Linearly -- Part 3 33x faster than Rust: Why I stopped waiting for my compiler and built my own. I Built My First Production AWS Project as a Career Changer Why Detecting PII Matters More Than Ever JSON Schema in 10 Minutes — Validation, Types & Real Examples Python Tasks How I Started My Cybersecurity Journey as an SQA Engineer 🔐 Why "fancy fonts" in Discord and Instagram bios turn into boxes ☁️ GKE private cluster setup — common mistakes and how to avoid them I Thought a Username Didn’t Matter… Until I Saw How Much People Care About It Claude for Small Business: 382K Day-One Buyer's Guide I Built a Diagnostic Toolkit for PyTorch Because I Was Tired of Guessing Why Models Fail How I Built an AI-Powered Incident RCA Platform with LangGraph and RAG The Paywall Was a Painted Door Sonnet hallucinated. My agent stored it as fact. How React-Style Time-Slicing Keeps UIs Responsive 这个 Princeton 开源项目让 AI 自己修 Bug,19K Stars 但 90% 的人只用了 1% 功能 🔥 SWE-agent's 5 Hidden Uses Nobody Told You About 🔥 Decompiling Serial Number U-36: Python TERCOM Reconstruction, Cryptographic Logistical Forensics, and Swarm Consensus Fault Tolerance Microservices Patterns
Camera2 API: Handling Orientation, Focus, and Exposure in Background — How to Keep Your Android Camera Running With the Screen Off
Super Funicu · 2026-05-26 · via DEV Community

Originally answered on Quora: "How do you record a video with the screen off in Android?". This is the dev.to canonical — the long, code-level version of the same architecture, written for developers shipping a camera-adjacent Android app.

If you've tried to build an Android camera app that records with the screen off, you've hit the wall I'm about to describe. The Camera2 session works fine while the app is foregrounded. You lock the screen. A few seconds later — sometimes thirty, sometimes ten minutes, sometimes after a Doze maintenance window — the frames stop. No exception, no broken pipe, no helpful log. Just silence.

This article is a tour of the four-system problem that creates that silence, and the architecture Background Camera RemoteStream (https://play.google.com/store/apps/details?id=com.superfunicular.digicam&utm_source=devto&utm_medium=article&utm_campaign=2026w22) uses to keep a Camera2 session alive across screen-off, Doze, App Standby, low-memory pressure, and the OEM-specific battery-saver patches that ship on real phones.

It also covers the two pieces the Quora answer left on the cutting-room floor: how to handle orientation when there's no Activity context to read rotation from, and how to keep focus and exposure sane across a multi-hour recording where lighting can change from sunset to streetlight without the user touching the device.

The four systems that will kill your camera session

You are not fighting one thing. You are fighting four overlapping power-management systems, and each one has a different lever:

  1. The foreground-service lifecycle. If your camera capture lives in a regular service, the system can kill it for memory pressure within seconds of the screen turning off. The lever here is ForegroundServiceType.CAMERA (added in API 29, required on API 34+) with a persistent notification.

  2. Doze mode. After the device has been stationary and unplugged for a while, the system batches background work into windows. Network calls are deferred, alarms are coalesced, and — critically for our case — wakelocks held without the right foreground-service type can be revoked. The lever is having the right foreground-service type, plus a partial wakelock with the correct tag.

  3. App Standby Buckets. Even if you survive Doze, the system bucketizes your app (active → working set → frequent → rare → restricted) based on user-perceived usage. A "rare"-bucket app can have its camera and network capabilities curtailed during periods of non-interaction. The lever is the foreground-service notification: as long as the user can see and dismiss it, the bucket stays warm.

  4. OEM patches. This is the layer the AOSP documentation does not warn you about. Xiaomi's MIUI, Huawei's EMUI, Samsung's One UI, OPPO's ColorOS, Vivo's FunTouchOS, OnePlus's OxygenOS — each one has a vendor battery optimizer that aggressively kills background camera and recording apps even when AOSP's rules say the app is allowed to keep running. The lever varies by OEM, but the universal shape is: a per-app "autostart" / "allow background activity" toggle that the user has to flip manually. You cannot programmatically opt in; the best you can do is detect the OEM, link the user to the right Settings page, and explain the trade-off.

If you only address #1, your session survives ten minutes on a clean Pixel and dies in three on a Xiaomi. If you address #1–#3 but not #4, you ship a 1-star review storm from users on every Chinese OEM. We learned this the hard way and rewrote the model.

The architecture in one diagram (described)

┌──────────────────────────────────────────────────┐
│ Activity (UI)                                    │
│  ─ binds to CameraService for status only        │
│  ─ does NOT own the CameraCaptureSession         │
└──────────────────────────────────────────────────┘
                    │ bind
                    ▼
┌──────────────────────────────────────────────────┐
│ CameraService (Foreground, type=camera|microphone)│
│  ─ holds the CameraDevice                        │
│  ─ holds the CameraCaptureSession                │
│  ─ holds a PARTIAL_WAKE_LOCK                     │
│  ─ posts the persistent notification             │
│  ─ owns a MediaRecorder OR a MediaCodec encoder   │
│  ─ also hosts the Ktor server (port 8080)        │
└──────────────────────────────────────────────────┘
                    │ frames
                    ▼
   ┌────────────────┴─────────────────┐
   ▼                                  ▼
File on local storage           Ktor HTTP handler
(MP4, hours of continuous)      (MJPEG/HLS to LAN viewer)

Enter fullscreen mode Exit fullscreen mode

The single most important architectural decision is: the Activity does not own the camera session. The Activity is a thin client that binds to the service and reads status. The service owns the CameraDevice, the CameraCaptureSession, the encoder, the wakelock, and the HTTP server. When the screen turns off, the Activity goes through its normal onPause → onStop lifecycle, and nothing happens to the camera session, because the session was never owned by the Activity in the first place.

This is the inverse of every Camera2 tutorial you've ever read. The tutorials open the camera in the Activity because they're showing you a viewfinder. We don't have a viewfinder when the screen is off, so the viewfinder ownership pattern is exactly wrong for our use case.

Step 1: The foreground service declaration

In AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<service
    android:name=".CameraService"
    android:enabled="true"
    android:exported="false"
    android:foregroundServiceType="camera|microphone" />

Enter fullscreen mode Exit fullscreen mode

The foregroundServiceType="camera|microphone" line is the one most tutorials get wrong post-API 30. Without it, calling startForeground() will throw ForegroundServiceTypeException on API 34+, and your service dies the moment you try to attach the camera.

In CameraService.onCreate() you call startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA or FOREGROUND_SERVICE_TYPE_MICROPHONE). The notification must be visible and non-dismissible. The user being able to see the recording is the price of being allowed to do the recording, and it's also a privacy win — we don't hide that we're using the camera.

Step 2: Opening the camera without a SurfaceView

This is where the AOSP samples stop being useful. Every sample opens the camera and feeds frames into a TextureView or SurfaceView so you can see the preview. With the screen off, the SurfaceView's surface is not valid; the window is not visible; you can't even create the view tree from a service in the first place.

The fix is to send the camera's output directly to surfaces that don't require a visible window:

  • A MediaRecorder.getSurface() for MP4 recording to local storage.
  • An ImageReader.getSurface() with ImageFormat.YUV_420_888 for the frame stream that goes to the HTTP server (we re-encode each frame as JPEG inline for the MJPEG response).

Both of these surfaces are background-valid. You build the SessionConfiguration with these as the only output targets — no preview surface at all — and the CameraCaptureSession will happily run for hours with the screen off.

val recorderSurface = mediaRecorder.surface
val imageReaderSurface = imageReader.surface

val outputs = listOf(
    OutputConfiguration(recorderSurface),
    OutputConfiguration(imageReaderSurface)
)

val sessionConfig = SessionConfiguration(
    SessionConfiguration.SESSION_REGULAR,
    outputs,
    executor,
    object : CameraCaptureSession.StateCallback() {
        override fun onConfigured(session: CameraCaptureSession) {
            val request = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD).apply {
                addTarget(recorderSurface)
                addTarget(imageReaderSurface)
                set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON)
                set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO)
            }.build()
            session.setRepeatingRequest(request, null, handler)
        }
        override fun onConfigureFailed(session: CameraCaptureSession) { /* … */ }
    }
)
cameraDevice.createCaptureSession(sessionConfig)

Enter fullscreen mode Exit fullscreen mode

The TEMPLATE_RECORD template is the right choice over TEMPLATE_PREVIEW because it sets the auto-exposure and auto-focus modes for sustained recording, not for a viewfinder-feedback loop. CONTROL_AF_MODE_CONTINUOUS_VIDEO keeps focus drifting smoothly with the scene rather than hunting; it's the same mode the system camera uses while filming.

Step 3: The wakelock

Foreground service + camera type is necessary but not always sufficient. On most stock Android builds, the foreground-service-camera type implicitly keeps the CPU awake. On some OEM builds it does not — particularly during long uninterrupted recording sessions where the system aggressively tries to deepen sleep. The defensive move is to take a PARTIAL_WAKE_LOCK while the camera session is running and release it the instant recording stops.

val pm = getSystemService(POWER_SERVICE) as PowerManager
val wakeLock = pm.newWakeLock(
    PowerManager.PARTIAL_WAKE_LOCK,
    "BackgroundCameraRemoteStream:CameraSession"
).apply { setReferenceCounted(false) }

// when starting:
wakeLock.acquire(8 * 60 * 60 * 1000L)  // 8h timeout

// when stopping:
if (wakeLock.isHeld) wakeLock.release()

Enter fullscreen mode Exit fullscreen mode

The timeout matters. An unbounded acquire() is a battery footgun; eight hours is the rough upper bound of a continuous recording session on a 4000mAh device, and the system will tear the session down anyway when the camera-capable battery floor is hit. If you somehow run longer, you can re-acquire on session resume.

The tag (BackgroundCameraRemoteStream:CameraSession) is not cosmetic. The system attributes wakelock cost to that tag in battery debug dumps; making it human-readable makes the battery-blame audit easier when a user reports drain.

Step 4: Orientation without an Activity

The orientation problem is the part most "background camera" tutorials skip. With a foreground Activity, you read display.rotation and feed it into your CaptureRequest. With the screen off and no Activity, the Display you'd want to query is dark and unreliable. We tried three approaches and only one of them holds.

What does not work: querying WindowManager.defaultDisplay.rotation from the service. On most devices it returns the last-known value, but on some Samsung and Xiaomi builds it returns ROTATION_0 regardless of how the device was oriented when the screen turned off — particularly if SensorManager was set to landscape-locked by the launcher.

What also does not work: subscribing to the OrientationEventListener during recording. The listener fires only when the screen is on. When the screen turns off, you stop getting callbacks. So you end up frozen on whatever the last reading was — which is fine 80% of the time and catastrophic the other 20%.

What does work: read Sensor.TYPE_ROTATION_VECTOR (or TYPE_GAME_ROTATION_VECTOR) directly from the SensorManager at recording start, derive a stable orientation from the rotation matrix, write it into the MediaRecorder.setOrientationHint() once, and don't change it during the session. The recording orientation is not actually expected to track the device through the recording — setOrientationHint() is a one-shot tag baked into the MP4 metadata. Any "rotation tracking" you want during the recording itself belongs in encoder-level metadata, not in the session config.

private fun stableOrientationAtRecordStart(): Int {
    val rotationVector = FloatArray(3)
    val rotationMatrix = FloatArray(9)
    // … listener that fills rotationVector, then:
    SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector)
    val orientations = FloatArray(3)
    SensorManager.getOrientation(rotationMatrix, orientations)
    val azimuthDeg = Math.toDegrees(orientations[0].toDouble())
    // Map azimuth → 0/90/180/270 for setOrientationHint
    return when {
        azimuthDeg in -45.0..45.0    -> 0
        azimuthDeg in 45.0..135.0    -> 90
        azimuthDeg in -135.0..-45.0  -> 270
        else                         -> 180
    }
}

Enter fullscreen mode Exit fullscreen mode

Two notes. First, on a phone you've mounted upside down (the very common case for an old phone clipped to a window with a phone holder), this gives you the correct upside-down orientation, which MediaRecorder will then bake in so the recording plays right-side-up on any video viewer. Second, if you want to expose the orientation to the LAN MJPEG viewer too, write the value to a shared AtomicInteger and read it in the Ktor handler's Content-Disposition or a custom X-Orientation header. Browsers won't act on the header but a custom viewer can.

Step 5: Auto-focus and auto-exposure across a multi-hour recording

TEMPLATE_RECORD gives you sensible starting modes (CONTROL_AF_MODE_CONTINUOUS_VIDEO and CONTROL_AE_MODE_ON) but it doesn't solve the hardest case: a baby monitor or security camera that records from late afternoon through deep night. Lighting changes by 6–8 stops over a few hours. Auto-exposure will silently clamp at min ISO and you'll see a long stretch of underexposed footage before the scene actually warrants it.

Three knobs to turn:

1. Cap the exposure-time ceiling. The system's default AE algorithm will happily push exposure to multi-hundred-millisecond shutter times in low light, which gives you motion-blurred mush on a baby monitor where you want to be able to see a baby moving. We cap at 1/30s (≈33ms) so motion is recognizable even at the cost of more grain.

val maxExposureTimeNs = 33_000_000L  // 1/30s
captureRequestBuilder.set(
    CaptureRequest.SENSOR_EXPOSURE_TIME,
    maxExposureTimeNs
)
captureRequestBuilder.set(
    CaptureRequest.CONTROL_AE_MODE,
    CaptureRequest.CONTROL_AE_MODE_OFF  // disable AE so the cap holds
)
captureRequestBuilder.set(
    CaptureRequest.SENSOR_SENSITIVITY,
    3200  // let ISO ride to compensate
)

Enter fullscreen mode Exit fullscreen mode

The trade-off is grain in dark rooms vs. blur in dark rooms. Grain you can see through. Blur you can't.

2. Disable AF hunting during recording. Continuous-video AF is great for an actively-filmed scene, terrible for a static-mount security camera that just wants to keep the same focal plane sharp. Switch to CONTROL_AF_MODE_OFF and lock the lens at a hand-picked diopter once the recording stabilizes. We auto-detect "the scene is static" by checking SENSOR_FRAME_DURATION variance over the first 60 seconds and lock AF if variance is low.

captureRequestBuilder.set(
    CaptureRequest.CONTROL_AF_MODE,
    CaptureRequest.CONTROL_AF_MODE_OFF
)
captureRequestBuilder.set(
    CaptureRequest.LENS_FOCUS_DISTANCE,
    stableFocusDiopter  // measured during the 60s warmup
)

Enter fullscreen mode Exit fullscreen mode

For an actively-filmed scene (a baby in a crib, a pet moving around) we keep CONTROL_AF_MODE_CONTINUOUS_VIDEO. The point isn't "always do X" — it's "detect the scene type once, then commit."

3. Fix the white balance. CONTROL_AWB_MODE_AUTO will drift visibly when a streetlight clicks on at dusk. Lock white balance to CONTROL_AWB_MODE_INCANDESCENT (or whatever fits) once the scene has stabilized. Same logic as AF: detect once, commit.

If you skip these three, your hours-long recording will look fine in the first ten minutes and progressively wrong over the next three hours. With them, the recording looks consistent end to end — which is exactly the property a baby monitor or security camera needs.

Step 6: Surviving Doze and the OEM kill

On AOSP, the foreground-service-camera type plus the wakelock plus the persistent notification is enough. On OEM Android, it isn't.

The defense is twofold:

First, request battery-optimization exemption. You don't get to set this programmatically — the user has to confirm — but you can route them to the right screen:

val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
    data = Uri.parse("package:$packageName")
}
startActivity(intent)

Enter fullscreen mode Exit fullscreen mode

Google's policy is to allow this prompt only for apps that demonstrably need it (alarm clocks, navigation, accessibility, etc.). A long-running camera that records with the screen off qualifies, and Play review has consistently accepted ours on that justification.

Second, detect the OEM and offer a one-tap deep-link to its specific autostart screen. The shape of the code is:

val manufacturer = Build.MANUFACTURER.lowercase()
val intent = when (manufacturer) {
    "xiaomi" -> Intent().setComponent(ComponentName(
        "com.miui.securitycenter",
        "com.miui.permcenter.autostart.AutoStartManagementActivity"))
    "huawei" -> Intent().setComponent(ComponentName(
        "com.huawei.systemmanager",
        "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"))
    "samsung" -> Intent().setComponent(ComponentName(
        "com.samsung.android.lool",
        "com.samsung.android.sm.ui.battery.BatteryActivity"))
    "oppo", "realme" -> Intent().setComponent(ComponentName(
        "com.coloros.safecenter",
        "com.coloros.privacypermissionsentry.PermissionTopActivity"))
    "vivo" -> Intent().setComponent(ComponentName(
        "com.vivo.permissionmanager",
        "com.vivo.permissionmanager.activity.BgStartUpManagerActivity"))
    "oneplus" -> Intent().setComponent(ComponentName(
        "com.oneplus.security",
        "com.oneplus.security.chainlaunch.view.ChainLaunchAppListActivity"))
    else -> null
}

Enter fullscreen mode Exit fullscreen mode

These component names are stable in practice but not contractual — OEMs occasionally rename activities between major versions. You wrap each startActivity() in a try/catch and fall back to the generic battery-optimization screen if the component is missing.

The user-experience principle: on first launch, after the user grants camera + audio, you run an OEM detection and show a one-screen wizard explaining that this OEM kills background recording aggressively, and offering a button that drops them on the right Settings page. Skipping this step is the single biggest cause of "the app stopped recording overnight" reviews in our crash and feedback corpus.

Step 7: Throwing the embedded HTTP server in

The reason any of this is worth doing is that once you have a long-running camera session, you can do interesting things with the frames. The two most valuable in our case are: write them to a local file (which the user retrieves over USB or by sharing from the app), and serve them on the LAN to a browser running on any other device.

We use Ktor as the embedded HTTP server, hosted inside the same foreground service. Ktor's embeddedServer(Netty, port = 8080) runs in its own coroutine scope; we hook the ImageReader.OnImageAvailableListener to a BroadcastChannel<ByteArray> that the Ktor handler subscribes to.

imageReader.setOnImageAvailableListener({ reader ->
    val image = reader.acquireLatestImage() ?: return@setOnImageAvailableListener
    val jpegBytes = yuvToJpeg(image, quality = 80)
    image.close()
    frameChannel.trySend(jpegBytes)
}, handler)

Enter fullscreen mode Exit fullscreen mode

The Ktor handler then serves it as multipart/x-mixed-replace (MJPEG), which is the cheapest streaming format that works in every browser without HLS infrastructure or WebRTC negotiation:

get("/stream.mjpeg") {
    call.respondOutputStream(ContentType("multipart", "x-mixed-replace; boundary=frame")) {
        val sub = frameChannel.openSubscription()
        for (frame in sub) {
            write("--frame\r\nContent-Type: image/jpeg\r\nContent-Length: ${frame.size}\r\n\r\n".toByteArray())
            write(frame)
            write("\r\n".toByteArray())
            flush()
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Two important details: bind to 0.0.0.0 not 127.0.0.1 so the LAN can reach you, and gate the endpoint with a simple token (PIN shown on the device, required as ?t=NNNN) so a roommate on the same Wi-Fi can't tap into your nursery feed. Local-only does not mean unauthenticated.

What we measured

After all seven steps, on a Pixel 6 (Android 14) we get continuous 1080p recording for 8h+ on a single charge with the screen off. On a Xiaomi Redmi Note 12 with autostart enabled, we get roughly the same. With autostart not enabled, the session dies on average at 47 minutes — which matches what the in-app review pattern was telling us before the OEM-wizard work landed. The OEM wizard moved the "session died overnight" support-email rate to roughly 1/100th of what it was.

The exposure-cap + AF-lock + AWB-lock combo (Step 5) was a separate before/after: in our internal dogfood we previously had reviews complaining about "blurry baby" or "the whole video went greenish after sunset." After committing those three, the same reviewers reported the recording was "watchable end to end."

Trade-offs we made on purpose

A few choices that look wrong if you skim the implementation:

No video preview in the recording Activity. A preview surface adds two more buffers in the camera pipeline, costs ~8% extra battery, and is unnecessary for our use case (the user already pointed the phone before locking the screen). When the user wants to verify framing, they open the LAN viewer in a browser on another device, which uses the same MJPEG endpoint that runs whether the recording Activity is open or not.

MJPEG over WebRTC for the LAN stream. WebRTC is technically superior — lower latency, adaptive bitrate, congestion-aware. But it requires a signaling server, which violates the local-only architectural promise. MJPEG works zero-config across the LAN. The latency penalty (~400ms vs. ~80ms) is invisible for a security/baby-monitor use case where the user is watching for events, not piloting a drone.

No background uploads anywhere. No "smart clip detection in the cloud," no "AI motion summaries," none of the features that would make our app score better on a feature-matrix comparison. We made this trade explicit and made it the architectural point. The reason is in our Meari breach architectural breakdown: the moment you add an authenticated cloud broker, you become subject to the failure mode that broke 378 brands of cameras in a single key-extraction in May 2026. The point of our architecture is to be structurally incapable of being that breach.

Cross-links for further reading

The actual app

If you want to use this rather than rebuild it, the production version of everything above ships in Background Camera RemoteStream on Google Play: https://play.google.com/store/apps/details?id=com.superfunicular.digicam&utm_source=devto&utm_medium=article&utm_campaign=2026w22 — free, no account, no cloud, recording stays local, LAN streaming via the embedded web server, optional YouTube Live streaming (Pro feature) on your own channel. Source-of-truth feedback channel is the Play Store reviews; we read every one.

Built by Super Funicular LLC. Website: https://superfunicular.com