
Android screen sharing over WebRTC stopped being a weekend hack the day Android 14 shipped. Half the patterns that worked in 2023 now throw a SecurityException and kill your process on the first tap. Get one call in the wrong order and the feature dies in front of your user. We’ve shipped this on production Android apps at Fora Soft since the MediaProjection API landed, so this guide is the version we hand a new engineer: every skeleton compiles, every bitrate is from real traffic, and every gotcha is one we’ve hit and fixed.
Key takeaways
• Stop using google-webrtc. The official prebuilt has been unmaintained since 2018. In 2026 use io.getstream:stream-webrtc-android (1.3.10) or LiveKit’s fork.
• Foreground service first, projection second. Android 14 enforces the order. Get it backwards and the OS throws SecurityException.
• 720p at 15 fps, 1.5 Mbps is the honest baseline for shared screens on cellular. Reach for 1080p only after you’ve confirmed Wi-Fi.
• Android 16 is the current target (June 2025). On large screens it lets apps fill the display, so your resize handling matters more, not less.
• It’s a lifecycle problem, not a codec problem. Nail the service order, the callbacks, and TURN failover, and the rest is tuning.
Why Fora Soft wrote this guide
We build real-time video for a living: 250+ products since 2005 with a 50-engineer in-house team, most of it WebRTC. WebRTC screen sharing is a feature we’ve shipped and re-shipped across Android versions, and the surface bites in predictable places. On BrainCert, a virtual-classroom platform at $3M ARR with 100K+ customers and 500M+ classroom minutes, tutors share their Android screens to walk students through a problem. On Sprii, a live-commerce app that’s driven €365M+ in sales, hosts share product close-ups mid-stream.
The honest reason this guide exists: most tutorials online are browser-only (getDisplayMedia in JavaScript) and skip the Android-specific traps entirely. This one is about the native path. If you want the wider Android WebRTC picture first, read our companion piece on WebRTC in Android: SDKs, capture, and Compose UI, or the primer on what WebRTC actually is.
Stuck on an Android screen-share bug you can’t reproduce?
We’ll look at your capture pipeline and tell you what’s wrong in 30 minutes — no slides, just engineers who’ve shipped this.
Android screen sharing in 60 seconds
The Android screen-share path has five moving parts, and skipping any one of them breaks the feature. Here they are in the order the frames actually flow.
1. MediaProjection. Android’s system API for capturing screen pixels. It asks the user for consent every single session, and there’s no way to skip the dialog.
2. Foreground service. From Android 14, projection has to run inside a foreground service of type mediaProjection, started before the projection is granted.
3. ScreenCapturerAndroid. The WebRTC library’s VideoCapturer that wraps a MediaProjection into a frame stream.
4. PeerConnection. The WebRTC abstraction that handles ICE, congestion control, and SRTP encryption to the far peer.
5. Signaling. An out-of-band channel (WebSocket is the usual pick) that trades SDP offers, answers, and ICE candidates. WebRTC doesn’t ship signaling; you bring your own.

Figure 1. The six parts of the pipeline. Part 3 — the foreground service — is where Android 14 breaks naive code.
Picking a WebRTC library in 2026
Start here, because most teams get this decision wrong. Google’s org.webrtc:google-webrtc has been unmaintained since 2018, and the prebuilt vanished when JCenter shut down. If a Stack Overflow answer tells you to depend on it, that answer is stale.
| Library | Status (2026) | Best for | License |
|---|---|---|---|
| getstream/webrtc-android | Active (1.3.10) | New builds, recent WebRTC, Compose support | BSD |
| webrtc-sdk/android (LiveKit) | Active | LiveKit apps, namespace isolation | Apache 2.0 |
| org.webrtc:google-webrtc | Dead since 2018 | Nothing — do not depend on it | BSD |
| LiveKit Android SDK | Active | High-level API plus managed infrastructure | Apache 2.0 |
| Daily Android SDK | Active | Daily.co customers, rooms out of the box | Proprietary |
Our pick for new builds: io.getstream:stream-webrtc-android. It tracks upstream WebRTC closely, the Kotlin APIs are pleasant, Compose helpers exist, and there are no license surprises. We’d pick it over the raw LiveKit SDK when we need direct control of the PeerConnection, and it’s not close.
// build.gradle.kts (Module: app)
dependencies {
implementation("io.getstream:stream-webrtc-android:1.3.10")
// optional Compose helpers
implementation("io.getstream:stream-webrtc-android-ui-compose:1.3.10")
}
Always check the GetStream/webrtc-android releases for the current version before you pin it.
Reach for a managed SDK (LiveKit, Daily, 100ms) when: you need screen share live in under a month and don’t yet have an Android engineer who’s fought foreground services. Reach for the raw getstream fork when you need per-frame control or you’re optimizing cost at scale.
Manifest permissions and the service type
Get the manifest right first, because a missing permission or the wrong service type fails silently or with a confusing exception. Two lines catch people out: FOREGROUND_SERVICE_MEDIA_PROJECTION is required from Android 14, and POST_NOTIFICATIONS is a runtime permission from Android 13 that you also have to request at runtime.
<manifest ...>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application ...>
<service
android:name=".ScreenShareService"
android:foregroundServiceType="mediaProjection"
android:exported="false" />
</application>
</manifest>
The foreground-service order that breaks Android 14
Here’s the single bug that generates the most Android 14 crash reports: from API 34 you must call startForeground before getMediaProjection. Do it the other way and the OS throws SecurityException and tears down the process. Same four calls, two orders, one ships.

Figure 2. The correct order on the left; the SecurityException path on the right.
class ScreenShareService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, id: Int): Int {
startForeground(
NOTIF_ID, buildNotification(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
)
// Only NOW is it safe to grab the projection
val code = intent!!.getIntExtra(EXTRA_RESULT_CODE, -1)
val data = intent.getParcelableExtra<Intent>(EXTRA_DATA)!!
val mp = mediaProjectionManager.getMediaProjection(code, data)
startCapture(mp)
return START_STICKY
}
}
One non-obvious detail: the projection’s resultCode and data Intent are single-use. Grab them in the Activity, pass them to the service via putExtra, and don’t try to reuse them.
Requesting the projection consent
The user approves every projection session, and you can’t suppress the dialog. The flow is: launch the capture intent, receive the result, hand it to the foreground service.
private val launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK && result.data != null) {
val svc = Intent(this, ScreenShareService::class.java).apply {
putExtra(EXTRA_RESULT_CODE, result.resultCode)
putExtra(EXTRA_DATA, result.data!!)
}
ContextCompat.startForegroundService(this, svc)
}
}
fun requestScreenShare() {
val mpm = getSystemService(MediaProjectionManager::class.java)
launcher.launch(mpm.createScreenCaptureIntent())
}
Wiring the capturer to a PeerConnection
Inside the service, hand the projection to ScreenCapturerAndroid, wrap it in a VideoSource, and add the track to your PeerConnection. The one flag that decides whether your share looks sharp or smeared is isScreencast=true.
private fun startCapture(projection: MediaProjection) {
val egl = EglBase.create()
val factory = PeerConnectionFactory.builder()
.setVideoEncoderFactory(DefaultVideoEncoderFactory(egl.eglBaseContext, true, true))
.setVideoDecoderFactory(DefaultVideoDecoderFactory(egl.eglBaseContext))
.createPeerConnectionFactory()
val capturer = ScreenCapturerAndroid(projectionData(projection),
object : MediaProjection.Callback() {
override fun onStop() { stopSelf() }
})
val helper = SurfaceTextureHelper.create("CaptureThread", egl.eglBaseContext)
val source = factory.createVideoSource(true) // isScreencast = true
capturer.initialize(helper, this, source.capturerObserver)
capturer.startCapture(1280, 720, 15) // width, height, fps
val track = factory.createVideoTrack("screen0", source)
peerConnection.addTrack(track, listOf("stream0"))
}
Why the flag matters: createVideoSource(true) selects WebRTC’s screen-optimized encoder profile — lower keyframe cadence, content-adaptive bitrate. Forgetting it is the most common reason a share looks terrible on the receiving end.
The bitrate budget that actually works
The realistic default is 1280×720 at 15 fps, targeting 1.5 Mbps with a 2.5 Mbps ceiling. It survives virtually every 4G connection, reads well for about 95% of screen content, and doesn’t melt a mid-range CPU. Teams that ship “1080p, 30 fps” off the marketing page drop frames the moment a user walks outside. The numbers below are from production traffic on our Android apps.

Figure 3. Bitrate by use case. Columns are anchored to zero; the solid block is the safe floor, the lighter block is Wi-Fi headroom.
| Use case | Resolution | FPS | Bitrate | Network |
|---|---|---|---|---|
| Slides / static | 1280×720 | 5–10 | 0.5–0.8 Mbps | 3G+ |
| App walkthrough | 1280×720 | 15 | 1.0–1.5 Mbps | 4G / Wi-Fi |
| Code walkthrough | 1920×1080 | 10–15 | 1.5–2.5 Mbps | Wi-Fi |
| Video playback | 1280×720 | 24–30 | 2–3 Mbps | Wi-Fi |
| High-quality 1080p | 1920×1080 | 30 | 3–5 Mbps | Wi-Fi only |
Set the ceiling explicitly through RtpSender.parameters.encodings[i].maxBitrateBps. Skip it and WebRTC’s congestion control will happily starve your audio and other tracks on the same connection.
Codecs to ship in 2026
Ship H.264 with a VP8 fallback and let the far end pick. That’s the boring, correct answer for screen share on Android in 2026, and here’s the reasoning behind each option.
H.264. Universal hardware encode support on Android, which is why it’s the safe default. It compresses text-heavy screens a little worse than VP9, and that’s a trade we take for reliability.
VP8. The historical WebRTC default. Software support is everywhere; hardware support varies. Slightly nicer than H.264 on screen content at the same bitrate.
VP9. Better compression on dense text, but hardware support is patchy on mid-range Android. Fine as an opt-in upgrade with a software fallback.
AV1. Best compression, roughly 3–5× the VP9 CPU cost. Hardware decoders are still rare on Android in 2026, so most devices software-decode and drain the battery. AV1’s screen-content coding is interesting for ultra-low-bitrate sharing (100–500 kbps), but mainstream device support lands around 2027–2028. We haven’t shipped it as a default yet, and we’d tell you not to either.
Reach for VP9 or AV1 when: the content is mostly static text (spreadsheets, code, documents), both peers have confirmed hardware support, and bandwidth is the binding constraint. Otherwise H.264 with VP8 fallback wins on reliability. If codecs are new to you, our primer on digital video covers the fundamentals.
Capturing system audio with the screen
If you need the audio a shared app is playing, Android 10 and up gives you AudioPlaybackCaptureConfiguration, which captures system playback during a projection session. It’s handy for sharing a screen with video or in-app sound.
val config = AudioPlaybackCaptureConfiguration.Builder(projection) .addMatchingUsage(AudioAttributes.USAGE_MEDIA) .addMatchingUsage(AudioAttributes.USAGE_GAME) .build() val record = AudioRecord.Builder() .setAudioPlaybackCaptureConfig(config) .setAudioFormat(format) .setBufferSizeInBytes(bufferSize) .build()
The catch: apps that set android:allowAudioPlaybackCapture="false" opt out entirely — banking and DRM apps usually do. Microphone input stays a separate AudioRecord; mix the two streams client-side or server-side depending on your use case.
Reach for system-audio capture when: you’re sharing a video, a game, or an app with sound cues. Skip it for plain UI walkthroughs — it’s one more permission and one more thing to fail, for audio nobody needs.
Android 14 to 16: what changed
Android 16 is the current release (it shipped in June 2025), and each version since Android 13 has tightened screen capture. You target the newest one but keep every older guard in place, because your users span all of them.

Figure 4. Each release added a constraint. Android 16 is current as of 2026.
Android 14 (2023). Foreground service of type mediaProjection became mandatory (see the order bug above). It also added a single-app capture picker, plus onCapturedContentResize() and onCapturedContentVisibilityChanged(). The UX assumption breaks here: users think they’re sharing slides, you think they’re sharing the home screen.
Android 15 (2024). Projection auto-stops when the device locks (resume needs re-consent), a persistent status-bar chip lets users kill the share, and notification bodies are masked during projection. You can’t suppress any of it, so design the re-share prompt.
Android 16 (2025, current). For apps targeting API 36, orientation and aspect-ratio restrictions no longer apply on large screens (smallest width 600dp and up) — the app fills the display window, so captured dimensions change more often and your onCapturedContentResize handling earns its keep. Android 16 also previews a local-network permission that will gate LAN peer and cast discovery in a future release; worth watching if you rely on same-network signaling. Register the callbacks once and react to them:
projection.registerCallback(object : MediaProjection.Callback() {
override fun onStop() { stopCapture() }
override fun onCapturedContentResize(w: Int, h: Int) {
capturer.changeCaptureFormat(w, h, 15)
}
override fun onCapturedContentVisibilityChanged(visible: Boolean) {
if (!visible) showResumePrompt()
}
}, handler)
Content protection with FLAG_SECURE and DRM
If you handle sensitive data — banking, healthcare, paid streaming — there are two directions to think about: what other apps’ FLAG_SECURE does to your capture, and what your own FLAG_SECURE does when someone captures you.
Capturing protected content. Windows that set FLAG_SECURE render as black in your frames. Hardware DRM (Widevine L1) is enforced at the chipset, so there’s no software workaround. Don’t promise users they can share Netflix or their banking app.
Protecting your own content. For screens that show PINs, OTPs, or PII, set window.setFlags(FLAG_SECURE, FLAG_SECURE) on those Activities. On recent Android, also watch the recording-state signal so a sensitive view can refuse to render while it’s being captured — defense in depth for compliance.
Signaling, ICE, and TURN
WebRTC needs a signaling channel it doesn’t provide, and it needs relay servers to survive mobile networks. On cellular, TURN isn’t optional: carrier-grade NAT blocks direct peering, so without a relay the connection simply never forms. Per the W3C WebRTC spec, signaling is your responsibility — WebSocket is the common choice.
ICE servers. Configure both STUN and TURN. STUN gets you direct connectivity in friendly NATs; TURN covers the cellular case. Real deployments run two or three STUN and one or two TURN servers across regions.
TURN providers. Twilio’s Network Traversal Service, Cloudflare’s TURN, Xirsys, or self-hosted coturn. Relayed traffic is billed by the gigabyte and it can dwarf your signaling and SFU costs at scale, which is the whole point of the cost model below.
Reconnect strategy. Screen-share sessions run long (10–30 minutes is normal), so the signaling socket will drop. Use exponential-backoff reconnect with heartbeats and re-negotiate SDP if the new path has a different IP topology. For the topology behind all of this — mesh, SFU, MCU — see our WebRTC architecture guide.
Not sure your TURN setup will hold at scale?
We’ll size your relay footprint and model the egress cost before you commit to a provider.
Build vs buy: the real cost
The dominant variable at runtime is relayed egress, so let’s do the arithmetic out loud. Take a 20-minute share at our default 720p / 15 fps / 1.5 Mbps, relayed through TURN in one direction:
Step 1 — data per session. 1.5 Mbit/s × 1,200 s = 1,800 Mbit = 225 MB, call it 0.22 GB.
Step 2 — egress cost. At a conservative managed-TURN rate of $0.40/GB, that’s 0.22 × $0.40 ≈ $0.09 per 20-minute relayed session.
Step 3 — the decision. At a few thousand sessions a month the relay bill is real but modest; self-hosting coturn on a bandwidth-included box pushes the marginal cost toward zero and you pay in ops instead. The build cost that actually dominates is engineering time on the testing matrix, not servers. For a fuller picture, our WebRTC development cost breakdown walks through real budgets, and if you’re weighing a managed SFU instead, compare the Agora alternatives.
Reach for self-hosted coturn when: you’re past roughly 50,000 relayed session-minutes a month and have someone to run it. Below that, a managed relay is cheaper once you price in the ops time you’d spend babysitting a coturn cluster.
Mini case: screen share on BrainCert
The situation. BrainCert’s tutors work across India and South-East Asia, where a lot of sessions run on mid-range Androids over shaky 4G. Their screen share — the feature tutors use to walk a student through a worked problem — looked fine on office Wi-Fi in the demo and fell apart in the field: choppy frames, occasional 30-second cut-offs.
The plan. We moved capture inside a proper foreground service (killing the cut-offs), pinned the encoder to 1280×720 / 15 fps with a 1.2 Mbps target and an explicit ceiling, switched the source to isScreencast=true, and put TURN failover in front of the flaky direct paths. We tested on Pixel, Samsung A-series, and Xiaomi Redmi under shaped-network conditions rather than office Wi-Fi.
The result. Sessions stopped dropping, the picture held together on sub-par 4G, and screen share went from a support-ticket generator to a feature tutors trust. Want a similar assessment of your capture stack? Book a 30-minute call and we’ll take a look.
Troubleshooting matrix
When something’s on fire in production, start here. These are the symptoms we hit most often and what actually fixes them.
| Symptom | Likely cause | Fix |
|---|---|---|
| SecurityException on getMediaProjection | Foreground service not started first (Android 14+) | Call startForeground before getMediaProjection |
| Black frames received | Captured app uses FLAG_SECURE or DRM | Can’t fix — document it for users |
| Choppy frames on cellular | Bitrate too high; congestion control starving | Cap at 1.5–2 Mbps; consider VP8 |
| Capture stops after 30 seconds | No foreground service; OS killed projection | Run capture inside a foreground service |
| Stretched image after rotation | No rotation handler | changeCaptureFormat on configuration change |
| No connection on cellular | No TURN; CGNAT blocking direct peer | Configure TURN over udp, tcp, and tls |
| Crash on Android 15+ after lock | Capture terminated; no resume handling | Listen for onStop and prompt to re-share |
One habit that pays for itself: log the key WebRTC stats (bitrate, packets lost, frame rate) per session to your backend. Without them you’re debugging user reports blind. And test on shaped networks — adb shell tc or a network conditioner — because that’s where field bugs reproduce.
Build it yourself in five questions
Answer these five and the build-vs-buy call usually makes itself.
1. Do you have an Android engineer who’s shipped a foreground service before? If not, the learning curve lands on your users. Partner or hire first.
2. What’s your timeline? Under a month means a managed SDK; the native testing matrix alone eats weeks.
3. How many products and regions? One app in one country rarely justifies the fixed build cost over per-minute managed pricing.
4. Do you need per-frame control? On-device redaction, custom encoders, or unusual layouts push you toward the raw library.
5. Is relay cost a scaling worry? Above ~50K relayed session-minutes a month, owning the stack (and coturn) starts to pay off. If two or more answers point at “build,” that’s usually the right call — and it’s where we can help.
When not to build this yourself
Straight talk: WebRTC screen sharing is one of the harder Android features to ship correctly, and there are three cases where you should buy or partner instead of building from scratch.
1. You need it in four weeks. A team new to WebRTC underestimates the device matrix. Ship on LiveKit Cloud, Daily, or 100ms in two weeks and replace it later if you must. Our take on building on LiveKit is a fair starting point.
2. You ship one product in one country. The fixed cost of building competes badly with paying a managed SFU per participant-minute.
3. You have no experienced Android engineer. Hire or partner — don’t learn foreground services on your customers. Screen share is also the layer where AI agents plug into WebRTC next, so building on a shaky base gets expensive fast.
FAQ
Can I capture the screen without showing the user a permission dialog?
No. The MediaProjection consent dialog is enforced by the OS on production devices and can’t be suppressed. Only privileged system apps preinstalled by the OEM with CAPTURE_VIDEO_OUTPUT can bypass it; consumer apps can’t.
Why do my captured frames show up as black?
The captured app has FLAG_SECURE set or is rendering DRM-protected content (Netflix, Disney+, banking apps). It’s enforced at the OS and hardware level, so there’s no workaround. Document the limitation rather than promising users they can share those apps.
Do I need a foreground service even if my app is already in the foreground?
From Android 14 on, yes. Even if your app is visible, projection must run inside a foreground service of type mediaProjection, started before you call getMediaProjection, or the OS throws SecurityException.
What frame rate should you pick for screen sharing?
15 fps is a solid default. Use 5–10 fps for static slides and documents, 24–30 fps for video playback. Higher frame rates burn bandwidth and CPU and rarely improve how static content reads.
Can I share the screen without WebRTC?
Yes. You can capture with MediaProjection and send over RTMP, HLS, or RTSP. WebRTC wins for low-latency, peer-to-peer or SFU scenarios; RTMP suits one-to-many broadcast where 3–6 seconds of latency is acceptable.
How do I capture audio along with the screen?
Use AudioPlaybackCaptureConfiguration (Android 10+) with the projection for system audio, and mix in the microphone via a separate AudioRecord if you need both. Apps that set allowAudioPlaybackCapture="false" can’t be captured; that’s the source app’s call.
What’s the maximum resolution I can capture?
It’s limited only by the display, but you should downscale to 1280×720 or 1920×1080 for transmission. Sending native resolution (say 3200×1440) wastes bandwidth, and the far end downscales it anyway.
Which Android versions should I support in 2026?
MediaProjection has existed since Android 5.0 (API 21), but the foreground-service rules changed sharply across Android 8, 10, 13, and 14. In 2026, supporting below Android 11 rarely pays off — over 95% of active devices run 11 or newer. Target Android 16 and keep the older guards.
Can I do this without writing native code?
Yes. LiveKit, Daily, and 100ms provide higher-level Android SDKs that wrap MediaProjection and WebRTC behind a simpler API. If you don’t need fine-grained control, that’s the fastest route to a working share.
What to read next
Android
WebRTC in Android: SDKs, capture, Compose
The wider Android WebRTC picture this guide sits inside.
Architecture
WebRTC architecture for business 2026
Mesh vs SFU vs MCU, and what to pick for your scale.
Cost
WebRTC development cost
What teams actually pay to ship WebRTC features.
Vendor
Agora.io alternatives compared
If you’re choosing a managed SFU instead of building.
Case study
BrainCert virtual classroom
Live screen sharing in production at $3M ARR.
Ready to ship Android screen sharing in 2026?
Pick a maintained fork (getstream/webrtc-android), declare a mediaProjection foreground service, start it before you ask for consent, hand the projection to ScreenCapturerAndroid, and add the track with isScreencast=true. Default to 720p / 15 fps / 1.5 Mbps, wire the Android 14+ resize and visibility callbacks, plan for lock-screen re-consent, and put TURN in front of cellular. Test on Pixel, Samsung, and Xiaomi under shaped networks, not office Wi-Fi.
We’ve shipped this across production Android apps — classrooms, live commerce, telehealth. If you’re starting fresh or unstalling a WebRTC project, the next step is a short scoping call where we’ll tell you the honest path.
Building WebRTC screen sharing on Android?
We’ll scope the feature, recommend the right stack, and give you a fixed-price estimate — from a team that’s done it before.
Prefer to plan the wider build first? Our video conferencing development service covers the full real-time stack, and our WebRTC development team page shows what we ship for clients.

