Android
video streaming
server
mobile development
camera API

Streaming video from Android camera to server

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

Streaming video from an Android device to a server is not just a camera problem. It is a pipeline problem: capture frames, encode them efficiently, choose a transport protocol, and make sure the server can ingest the stream without falling behind.

Pick The Right Transport First

Before writing camera code, decide what the server needs.

  • Use WebRTC for low-latency two-way communication.
  • Use RTMP or SRT for broadcast-style publishing to a media server.
  • Use a simple WebSocket or HTTP upload only for prototypes, diagnostics, or low-frame-rate computer-vision pipelines.

A common mistake is trying to send raw camera frames directly over HTTP as if that were equivalent to video streaming. It works for a prototype, but it wastes bandwidth and CPU because each frame is large and independently encoded.

In production, most systems capture camera frames, encode them with MediaCodec as H.264, and then send the encoded stream using a protocol the server already understands.

A Simple Prototype With CameraX And WebSocket

The following Android example is intentionally simple. It captures frames with CameraX, compresses them as JPEG, and sends them to a WebSocket server. That is not the most efficient production architecture, but it is easy to understand and runnable.

kotlin
1import android.graphics.ImageFormat
2import android.graphics.Rect
3import android.graphics.YuvImage
4import androidx.camera.core.ImageAnalysis
5import androidx.camera.core.ImageProxy
6import okhttp3.OkHttpClient
7import okhttp3.Request
8import okhttp3.WebSocket
9import java.io.ByteArrayOutputStream
10import java.nio.ByteBuffer
11
12class FrameStreamer(serverUrl: String) : ImageAnalysis.Analyzer {
13    private val webSocket: WebSocket = OkHttpClient()
14        .newWebSocket(
15            Request.Builder().url(serverUrl).build(),
16            SimpleWebSocketListener()
17        )
18
19    override fun analyze(image: ImageProxy) {
20        val jpegBytes = imageProxyToJpeg(image)
21        if (jpegBytes != null) {
22            webSocket.send(okio.ByteString.of(*jpegBytes))
23        }
24        image.close()
25    }
26
27    private fun imageProxyToJpeg(image: ImageProxy): ByteArray? {
28        val yPlane = image.planes[0].buffer.toByteArray()
29        val uPlane = image.planes[1].buffer.toByteArray()
30        val vPlane = image.planes[2].buffer.toByteArray()
31
32        val nv21 = ByteArray(yPlane.size + uPlane.size + vPlane.size)
33        System.arraycopy(yPlane, 0, nv21, 0, yPlane.size)
34        System.arraycopy(vPlane, 0, nv21, yPlane.size, vPlane.size)
35        System.arraycopy(uPlane, 0, nv21, yPlane.size + vPlane.size, uPlane.size)
36
37        val yuv = YuvImage(nv21, ImageFormat.NV21, image.width, image.height, null)
38        val out = ByteArrayOutputStream()
39        yuv.compressToJpeg(Rect(0, 0, image.width, image.height), 70, out)
40        return out.toByteArray()
41    }
42
43    private fun ByteBuffer.toByteArray(): ByteArray {
44        rewind()
45        val bytes = ByteArray(remaining())
46        get(bytes)
47        return bytes
48    }
49}

You would attach that analyzer to an ImageAnalysis use case in CameraX and point it at a ws:// endpoint on your server.

A Matching Python Server

A minimal Python receiver can accept the binary messages and write them to disk. This proves the transport works end to end.

python
1import asyncio
2from pathlib import Path
3import websockets
4
5output_dir = Path("frames")
6output_dir.mkdir(exist_ok=True)
7
8async def handler(websocket):
9    index = 0
10    async for message in websocket:
11        frame_path = output_dir / f"frame-{index:06d}.jpg"
12        frame_path.write_bytes(message)
13        index += 1
14
15async def main():
16    async with websockets.serve(handler, "0.0.0.0", 8765, max_size=4_000_000):
17        await asyncio.Future()
18
19asyncio.run(main())

This is enough for a prototype, a remote snapshot feed, or a CV ingestion service that analyzes individual frames.

What A Production Pipeline Usually Looks Like

For real video streaming, replace JPEG-per-frame transmission with encoded video.

The common Android path is:

  1. capture with CameraX or Camera2
  2. encode with MediaCodec
  3. push the encoded stream to an ingestion server

If the destination is a media server such as NGINX with RTMP support, Ant Media, or a WebRTC gateway, the server usually expects timestamped encoded packets instead of standalone images.

That matters because bandwidth drops sharply once frames are encoded as continuous video rather than independent JPEGs. Latency and battery use also improve.

Keep Backpressure Under Control

The camera can produce frames faster than the network can send them. If the upload path blocks, the app becomes unstable or memory usage spikes.

Use backpressure intentionally. With CameraX ImageAnalysis, configure a strategy that drops old frames instead of queueing indefinitely. For example, a live analytics or streaming path usually cares about the newest frame, not every historical frame.

If you move to MediaCodec, the same principle applies: keep the encoder and network sender decoupled so a slow server does not stall camera capture.

Security And Reliability

Do not ship an unauthenticated raw socket endpoint that accepts arbitrary uploads from the internet. At minimum, protect the stream with:

  • TLS
  • per-device authentication
  • server-side rate limits
  • short-lived stream credentials

Also plan for reconnect logic. Mobile networks drop, rotate IPs, and change bandwidth frequently. A robust client should tolerate reconnects and resume streaming instead of assuming a perfect connection.

Common Pitfalls

  • Sending raw or JPEG frames in production when the server really expects encoded video.
  • Ignoring backpressure and letting frame queues grow without limit.
  • Using the deprecated camera API when CameraX or Camera2 would be easier to maintain.
  • Forgetting that network jitter, not camera capture, often dominates stream quality.
  • Treating transport choice as an afterthought instead of the main architectural decision.

Summary

  • Android video streaming is a capture, encode, and transport pipeline.
  • 'WebSocket frame upload is fine for a prototype, but WebRTC, RTMP, or SRT are better production transports.'
  • 'CameraX is a practical way to capture frames on modern Android.'
  • Production systems usually encode with MediaCodec instead of sending standalone JPEG frames.
  • Backpressure, reconnection, and authentication matter as much as camera code.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.