package dev.mleku.h264cam; import android.Manifest; import android.app.Activity; import android.content.pm.PackageManager; import android.graphics.Matrix; import android.graphics.RectF; import android.graphics.SurfaceTexture; import android.hardware.camera2.*; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.util.Log; import android.view.Surface; import android.view.TextureView; import android.view.WindowManager; import android.widget.TextView; import java.io.*; import java.net.*; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; public class MainActivity extends Activity { static final String TAG = "H264Cam"; static final int PORT = 8080; static final int WIDTH = 1920; static final int HEIGHT = 1080; static final int FPS = 25; static final int BITRATE = 16_000_000; TextureView preview; TextView status, modeBtn; boolean dayMode = true; Surface previewSurface; HandlerThread camThread; Handler camHandler; CameraDevice camera; MediaCodec encoder; Surface encoderSurface; CameraCaptureSession captureSession; ServerSocket server; final CopyOnWriteArrayList clients = new CopyOnWriteArrayList<>(); byte[] sps, pps; int maxIso = 6400; @Override protected void onCreate(Bundle saved) { super.onCreate(saved); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().getDecorView().setSystemUiVisibility( android.view.View.SYSTEM_UI_FLAG_FULLSCREEN | android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION); if (getActionBar() != null) getActionBar().hide(); setContentView(R.layout.activity_main); preview = findViewById(R.id.preview); status = findViewById(R.id.status); modeBtn = findViewById(R.id.modeBtn); modeBtn.setOnClickListener(v -> toggleMode()); if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { requestPermissions(new String[]{Manifest.permission.CAMERA}, 1); } else { init(); } } @Override public void onRequestPermissionsResult(int req, String[] perms, int[] results) { if (results.length > 0 && results[0] == PackageManager.PERMISSION_GRANTED) { init(); } else { status.setText("camera permission denied"); } } void init() { camThread = new HandlerThread("cam"); camThread.start(); camHandler = new Handler(camThread.getLooper()); try { CameraManager mgr = (CameraManager) getSystemService(CAMERA_SERVICE); CameraCharacteristics chars = mgr.getCameraCharacteristics(mgr.getCameraIdList()[0]); android.util.Range isoRange = chars.get( CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE); if (isoRange != null) maxIso = isoRange.getUpper(); Log.i(TAG, "max ISO: " + maxIso); } catch (Exception e) {} setupEncoder(); startHttpServer(); preview.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { public void onSurfaceTextureAvailable(SurfaceTexture st, int w, int h) { configureTransform(w, h); openCamera(); } public void onSurfaceTextureSizeChanged(SurfaceTexture st, int w, int h) { configureTransform(w, h); } public boolean onSurfaceTextureDestroyed(SurfaceTexture st) { return true; } public void onSurfaceTextureUpdated(SurfaceTexture st) {} }); } // From Google's Camera2Basic sample void configureTransform(int viewWidth, int viewHeight) { int rotation = getWindowManager().getDefaultDisplay().getRotation(); Matrix matrix = new Matrix(); RectF viewRect = new RectF(0, 0, viewWidth, viewHeight); RectF bufferRect = new RectF(0, 0, HEIGHT, WIDTH); float cx = viewRect.centerX(); float cy = viewRect.centerY(); if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { bufferRect.offset(cx - bufferRect.centerX(), cy - bufferRect.centerY()); matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); float scale = Math.min( (float) viewHeight / HEIGHT, (float) viewWidth / WIDTH); matrix.postScale(scale, scale, cx, cy); matrix.postRotate(90 * (rotation - 2), cx, cy); } else if (rotation == Surface.ROTATION_180) { matrix.postRotate(180, cx, cy); } preview.setTransform(matrix); } void setupEncoder() { try { MediaFormat fmt = MediaFormat.createVideoFormat("video/avc", WIDTH, HEIGHT); fmt.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE); fmt.setInteger(MediaFormat.KEY_FRAME_RATE, FPS); fmt.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); fmt.setInteger(MediaFormat.KEY_BITRATE_MODE, MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR); fmt.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh); fmt.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); encoder = MediaCodec.createEncoderByType("video/avc"); encoder.setCallback(new MediaCodec.Callback() { public void onInputBufferAvailable(MediaCodec codec, int index) {} public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) { ByteBuffer buf = codec.getOutputBuffer(index); if (buf != null && info.size > 0) { byte[] data = new byte[info.size]; buf.get(data); broadcast(data); } codec.releaseOutputBuffer(index, false); } public void onError(MediaCodec codec, MediaCodec.CodecException e) { Log.e(TAG, "encoder error", e); } public void onOutputFormatChanged(MediaCodec codec, MediaFormat fmt) { ByteBuffer spsBuf = fmt.getByteBuffer("csd-0"); ByteBuffer ppsBuf = fmt.getByteBuffer("csd-1"); if (spsBuf != null) { sps = new byte[spsBuf.remaining()]; spsBuf.get(sps); } if (ppsBuf != null) { pps = new byte[ppsBuf.remaining()]; ppsBuf.get(pps); } Log.i(TAG, "got SPS/PPS"); } }, camHandler); encoder.configure(fmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); encoderSurface = encoder.createInputSurface(); encoder.start(); } catch (Exception e) { Log.e(TAG, "encoder setup failed", e); } } void openCamera() { try { CameraManager mgr = (CameraManager) getSystemService(CAMERA_SERVICE); String id = mgr.getCameraIdList()[0]; mgr.openCamera(id, new CameraDevice.StateCallback() { public void onOpened(CameraDevice cam) { camera = cam; startCapture(); } public void onDisconnected(CameraDevice cam) { cam.close(); camera = null; } public void onError(CameraDevice cam, int err) { cam.close(); camera = null; } }, camHandler); } catch (Exception e) { Log.e(TAG, "camera open failed", e); } } CaptureRequest buildRequest() throws CameraAccessException { CaptureRequest.Builder req = camera.createCaptureRequest( CameraDevice.TEMPLATE_RECORD); req.addTarget(previewSurface); req.addTarget(encoderSurface); if (dayMode) { req.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF); req.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 1_000_000L); // 1ms shutter req.set(CaptureRequest.SENSOR_SENSITIVITY, 100); // lowest practical ISO } else { req.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF); req.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 100_000_000L); // 100ms req.set(CaptureRequest.SENSOR_SENSITIVITY, maxIso); } return req.build(); } void toggleMode() { dayMode = !dayMode; modeBtn.setText(dayMode ? "DAY" : "NIGHT"); if (captureSession == null || camera == null) return; try { captureSession.setRepeatingRequest(buildRequest(), null, camHandler); } catch (Exception e) { Log.e(TAG, "mode switch failed", e); } } void startCapture() { try { SurfaceTexture tex = preview.getSurfaceTexture(); tex.setDefaultBufferSize(WIDTH, HEIGHT); previewSurface = new Surface(tex); camera.createCaptureSession( Arrays.asList(previewSurface, encoderSurface), new CameraCaptureSession.StateCallback() { public void onConfigured(CameraCaptureSession session) { captureSession = session; try { session.setRepeatingRequest(buildRequest(), null, camHandler); runOnUiThread(() -> showStatus()); } catch (Exception e) { Log.e(TAG, "capture failed", e); } } public void onConfigureFailed(CameraCaptureSession session) { Log.e(TAG, "session config failed"); } }, camHandler); } catch (Exception e) { Log.e(TAG, "startCapture failed", e); } } void broadcast(byte[] data) { if (clients.isEmpty()) return; List dead = new ArrayList<>(); for (OutputStream out : clients) { try { out.write(data); out.flush(); } catch (IOException e) { dead.add(out); } } clients.removeAll(dead); if (!dead.isEmpty()) runOnUiThread(this::showStatus); } void startHttpServer() { new Thread(() -> { try { server = new ServerSocket(PORT); Log.i(TAG, "HTTP server on port " + PORT); runOnUiThread(this::showStatus); while (!server.isClosed()) { Socket sock = server.accept(); new Thread(() -> handleClient(sock)).start(); } } catch (IOException e) { Log.e(TAG, "server error", e); } }).start(); } void handleClient(Socket sock) { try { BufferedReader in = new BufferedReader( new InputStreamReader(sock.getInputStream())); String line = in.readLine(); Log.i(TAG, "client: " + sock.getInetAddress() + " " + line); while ((line = in.readLine()) != null && !line.isEmpty()) {} OutputStream out = sock.getOutputStream(); out.write(("HTTP/1.1 200 OK\r\n" + "Content-Type: video/h264\r\n" + "Connection: close\r\n" + "Cache-Control: no-cache\r\n" + "\r\n").getBytes()); if (sps != null) out.write(sps); if (pps != null) out.write(pps); out.flush(); clients.add(out); runOnUiThread(this::showStatus); try { while (sock.getInputStream().read() != -1) {} } catch (IOException e) {} clients.remove(out); runOnUiThread(this::showStatus); } catch (IOException e) {} } void showStatus() { String ip = getLocalIp(); status.setText("http://" + ip + ":" + PORT + "/video " + WIDTH + "x" + HEIGHT + " " + FPS + "fps H264 " + (BITRATE/1_000_000) + "Mbps " + clients.size() + " client(s)"); } String getLocalIp() { try { for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) { NetworkInterface intf = en.nextElement(); for (Enumeration addrs = intf.getInetAddresses(); addrs.hasMoreElements();) { InetAddress addr = addrs.nextElement(); if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) { return addr.getHostAddress(); } } } } catch (Exception e) {} return "unknown"; } @Override protected void onDestroy() { super.onDestroy(); if (captureSession != null) captureSession.close(); if (camera != null) camera.close(); if (encoder != null) { encoder.stop(); encoder.release(); } if (server != null) try { server.close(); } catch (IOException e) {} if (camThread != null) camThread.quitSafely(); } }