MainActivity.java raw

   1  package dev.mleku.h264cam;
   2  
   3  import android.Manifest;
   4  import android.app.Activity;
   5  import android.content.pm.PackageManager;
   6  import android.graphics.Matrix;
   7  import android.graphics.RectF;
   8  import android.graphics.SurfaceTexture;
   9  import android.hardware.camera2.*;
  10  import android.media.MediaCodec;
  11  import android.media.MediaCodecInfo;
  12  import android.media.MediaFormat;
  13  import android.os.Bundle;
  14  import android.os.Handler;
  15  import android.os.HandlerThread;
  16  import android.util.Log;
  17  import android.view.Surface;
  18  import android.view.TextureView;
  19  import android.view.WindowManager;
  20  import android.widget.TextView;
  21  
  22  import java.io.*;
  23  import java.net.*;
  24  import java.nio.ByteBuffer;
  25  import java.util.*;
  26  import java.util.concurrent.CopyOnWriteArrayList;
  27  
  28  public class MainActivity extends Activity {
  29      static final String TAG = "H264Cam";
  30      static final int PORT = 8080;
  31      static final int WIDTH = 1920;
  32      static final int HEIGHT = 1080;
  33      static final int FPS = 25;
  34      static final int BITRATE = 16_000_000;
  35  
  36      TextureView preview;
  37      TextView status, modeBtn;
  38      boolean dayMode = true;
  39      Surface previewSurface;
  40      HandlerThread camThread;
  41      Handler camHandler;
  42      CameraDevice camera;
  43      MediaCodec encoder;
  44      Surface encoderSurface;
  45      CameraCaptureSession captureSession;
  46      ServerSocket server;
  47      final CopyOnWriteArrayList<OutputStream> clients = new CopyOnWriteArrayList<>();
  48      byte[] sps, pps;
  49      int maxIso = 6400;
  50  
  51      @Override
  52      protected void onCreate(Bundle saved) {
  53          super.onCreate(saved);
  54          getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
  55          getWindow().getDecorView().setSystemUiVisibility(
  56              android.view.View.SYSTEM_UI_FLAG_FULLSCREEN |
  57              android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
  58              android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
  59              android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
  60              android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
  61          if (getActionBar() != null) getActionBar().hide();
  62          setContentView(R.layout.activity_main);
  63          preview = findViewById(R.id.preview);
  64          status = findViewById(R.id.status);
  65          modeBtn = findViewById(R.id.modeBtn);
  66          modeBtn.setOnClickListener(v -> toggleMode());
  67  
  68          if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
  69              requestPermissions(new String[]{Manifest.permission.CAMERA}, 1);
  70          } else {
  71              init();
  72          }
  73      }
  74  
  75      @Override
  76      public void onRequestPermissionsResult(int req, String[] perms, int[] results) {
  77          if (results.length > 0 && results[0] == PackageManager.PERMISSION_GRANTED) {
  78              init();
  79          } else {
  80              status.setText("camera permission denied");
  81          }
  82      }
  83  
  84      void init() {
  85          camThread = new HandlerThread("cam");
  86          camThread.start();
  87          camHandler = new Handler(camThread.getLooper());
  88  
  89          try {
  90              CameraManager mgr = (CameraManager) getSystemService(CAMERA_SERVICE);
  91              CameraCharacteristics chars = mgr.getCameraCharacteristics(mgr.getCameraIdList()[0]);
  92              android.util.Range<Integer> isoRange = chars.get(
  93                  CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE);
  94              if (isoRange != null) maxIso = isoRange.getUpper();
  95              Log.i(TAG, "max ISO: " + maxIso);
  96          } catch (Exception e) {}
  97  
  98          setupEncoder();
  99          startHttpServer();
 100  
 101          preview.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
 102              public void onSurfaceTextureAvailable(SurfaceTexture st, int w, int h) {
 103                  configureTransform(w, h);
 104                  openCamera();
 105              }
 106              public void onSurfaceTextureSizeChanged(SurfaceTexture st, int w, int h) {
 107                  configureTransform(w, h);
 108              }
 109              public boolean onSurfaceTextureDestroyed(SurfaceTexture st) { return true; }
 110              public void onSurfaceTextureUpdated(SurfaceTexture st) {}
 111          });
 112      }
 113  
 114      // From Google's Camera2Basic sample
 115      void configureTransform(int viewWidth, int viewHeight) {
 116          int rotation = getWindowManager().getDefaultDisplay().getRotation();
 117          Matrix matrix = new Matrix();
 118          RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
 119          RectF bufferRect = new RectF(0, 0, HEIGHT, WIDTH);
 120          float cx = viewRect.centerX();
 121          float cy = viewRect.centerY();
 122  
 123          if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
 124              bufferRect.offset(cx - bufferRect.centerX(), cy - bufferRect.centerY());
 125              matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
 126              float scale = Math.min(
 127                  (float) viewHeight / HEIGHT,
 128                  (float) viewWidth / WIDTH);
 129              matrix.postScale(scale, scale, cx, cy);
 130              matrix.postRotate(90 * (rotation - 2), cx, cy);
 131          } else if (rotation == Surface.ROTATION_180) {
 132              matrix.postRotate(180, cx, cy);
 133          }
 134  
 135          preview.setTransform(matrix);
 136      }
 137  
 138      void setupEncoder() {
 139          try {
 140              MediaFormat fmt = MediaFormat.createVideoFormat("video/avc", WIDTH, HEIGHT);
 141              fmt.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE);
 142              fmt.setInteger(MediaFormat.KEY_FRAME_RATE, FPS);
 143              fmt.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
 144              fmt.setInteger(MediaFormat.KEY_BITRATE_MODE,
 145                  MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
 146              fmt.setInteger(MediaFormat.KEY_PROFILE,
 147                  MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
 148              fmt.setInteger(MediaFormat.KEY_COLOR_FORMAT,
 149                  MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
 150  
 151              encoder = MediaCodec.createEncoderByType("video/avc");
 152              encoder.setCallback(new MediaCodec.Callback() {
 153                  public void onInputBufferAvailable(MediaCodec codec, int index) {}
 154                  public void onOutputBufferAvailable(MediaCodec codec, int index,
 155                          MediaCodec.BufferInfo info) {
 156                      ByteBuffer buf = codec.getOutputBuffer(index);
 157                      if (buf != null && info.size > 0) {
 158                          byte[] data = new byte[info.size];
 159                          buf.get(data);
 160                          broadcast(data);
 161                      }
 162                      codec.releaseOutputBuffer(index, false);
 163                  }
 164                  public void onError(MediaCodec codec, MediaCodec.CodecException e) {
 165                      Log.e(TAG, "encoder error", e);
 166                  }
 167                  public void onOutputFormatChanged(MediaCodec codec, MediaFormat fmt) {
 168                      ByteBuffer spsBuf = fmt.getByteBuffer("csd-0");
 169                      ByteBuffer ppsBuf = fmt.getByteBuffer("csd-1");
 170                      if (spsBuf != null) { sps = new byte[spsBuf.remaining()]; spsBuf.get(sps); }
 171                      if (ppsBuf != null) { pps = new byte[ppsBuf.remaining()]; ppsBuf.get(pps); }
 172                      Log.i(TAG, "got SPS/PPS");
 173                  }
 174              }, camHandler);
 175              encoder.configure(fmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 176              encoderSurface = encoder.createInputSurface();
 177              encoder.start();
 178          } catch (Exception e) {
 179              Log.e(TAG, "encoder setup failed", e);
 180          }
 181      }
 182  
 183      void openCamera() {
 184          try {
 185              CameraManager mgr = (CameraManager) getSystemService(CAMERA_SERVICE);
 186              String id = mgr.getCameraIdList()[0];
 187              mgr.openCamera(id, new CameraDevice.StateCallback() {
 188                  public void onOpened(CameraDevice cam) {
 189                      camera = cam;
 190                      startCapture();
 191                  }
 192                  public void onDisconnected(CameraDevice cam) { cam.close(); camera = null; }
 193                  public void onError(CameraDevice cam, int err) { cam.close(); camera = null; }
 194              }, camHandler);
 195          } catch (Exception e) {
 196              Log.e(TAG, "camera open failed", e);
 197          }
 198      }
 199  
 200      CaptureRequest buildRequest() throws CameraAccessException {
 201          CaptureRequest.Builder req = camera.createCaptureRequest(
 202              CameraDevice.TEMPLATE_RECORD);
 203          req.addTarget(previewSurface);
 204          req.addTarget(encoderSurface);
 205          if (dayMode) {
 206              req.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);
 207              req.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 1_000_000L); // 1ms shutter
 208              req.set(CaptureRequest.SENSOR_SENSITIVITY, 100);          // lowest practical ISO
 209          } else {
 210              req.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);
 211              req.set(CaptureRequest.SENSOR_EXPOSURE_TIME, 100_000_000L); // 100ms
 212              req.set(CaptureRequest.SENSOR_SENSITIVITY, maxIso);
 213          }
 214          return req.build();
 215      }
 216  
 217      void toggleMode() {
 218          dayMode = !dayMode;
 219          modeBtn.setText(dayMode ? "DAY" : "NIGHT");
 220          if (captureSession == null || camera == null) return;
 221          try {
 222              captureSession.setRepeatingRequest(buildRequest(), null, camHandler);
 223          } catch (Exception e) {
 224              Log.e(TAG, "mode switch failed", e);
 225          }
 226      }
 227  
 228      void startCapture() {
 229          try {
 230              SurfaceTexture tex = preview.getSurfaceTexture();
 231              tex.setDefaultBufferSize(WIDTH, HEIGHT);
 232              previewSurface = new Surface(tex);
 233  
 234              camera.createCaptureSession(
 235                  Arrays.asList(previewSurface, encoderSurface),
 236                  new CameraCaptureSession.StateCallback() {
 237                      public void onConfigured(CameraCaptureSession session) {
 238                          captureSession = session;
 239                          try {
 240                              session.setRepeatingRequest(buildRequest(), null, camHandler);
 241                              runOnUiThread(() -> showStatus());
 242                          } catch (Exception e) {
 243                              Log.e(TAG, "capture failed", e);
 244                          }
 245                      }
 246                      public void onConfigureFailed(CameraCaptureSession session) {
 247                          Log.e(TAG, "session config failed");
 248                      }
 249                  }, camHandler);
 250          } catch (Exception e) {
 251              Log.e(TAG, "startCapture failed", e);
 252          }
 253      }
 254  
 255      void broadcast(byte[] data) {
 256          if (clients.isEmpty()) return;
 257          List<OutputStream> dead = new ArrayList<>();
 258          for (OutputStream out : clients) {
 259              try {
 260                  out.write(data);
 261                  out.flush();
 262              } catch (IOException e) {
 263                  dead.add(out);
 264              }
 265          }
 266          clients.removeAll(dead);
 267          if (!dead.isEmpty()) runOnUiThread(this::showStatus);
 268      }
 269  
 270      void startHttpServer() {
 271          new Thread(() -> {
 272              try {
 273                  server = new ServerSocket(PORT);
 274                  Log.i(TAG, "HTTP server on port " + PORT);
 275                  runOnUiThread(this::showStatus);
 276                  while (!server.isClosed()) {
 277                      Socket sock = server.accept();
 278                      new Thread(() -> handleClient(sock)).start();
 279                  }
 280              } catch (IOException e) {
 281                  Log.e(TAG, "server error", e);
 282              }
 283          }).start();
 284      }
 285  
 286      void handleClient(Socket sock) {
 287          try {
 288              BufferedReader in = new BufferedReader(
 289                  new InputStreamReader(sock.getInputStream()));
 290              String line = in.readLine();
 291              Log.i(TAG, "client: " + sock.getInetAddress() + " " + line);
 292  
 293              while ((line = in.readLine()) != null && !line.isEmpty()) {}
 294  
 295              OutputStream out = sock.getOutputStream();
 296              out.write(("HTTP/1.1 200 OK\r\n" +
 297                  "Content-Type: video/h264\r\n" +
 298                  "Connection: close\r\n" +
 299                  "Cache-Control: no-cache\r\n" +
 300                  "\r\n").getBytes());
 301  
 302              if (sps != null) out.write(sps);
 303              if (pps != null) out.write(pps);
 304              out.flush();
 305  
 306              clients.add(out);
 307              runOnUiThread(this::showStatus);
 308  
 309              try {
 310                  while (sock.getInputStream().read() != -1) {}
 311              } catch (IOException e) {}
 312  
 313              clients.remove(out);
 314              runOnUiThread(this::showStatus);
 315          } catch (IOException e) {}
 316      }
 317  
 318      void showStatus() {
 319          String ip = getLocalIp();
 320          status.setText("http://" + ip + ":" + PORT + "/video  " +
 321              WIDTH + "x" + HEIGHT + " " + FPS + "fps H264 " +
 322              (BITRATE/1_000_000) + "Mbps  " +
 323              clients.size() + " client(s)");
 324      }
 325  
 326      String getLocalIp() {
 327          try {
 328              for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
 329                      en.hasMoreElements();) {
 330                  NetworkInterface intf = en.nextElement();
 331                  for (Enumeration<InetAddress> addrs = intf.getInetAddresses();
 332                          addrs.hasMoreElements();) {
 333                      InetAddress addr = addrs.nextElement();
 334                      if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) {
 335                          return addr.getHostAddress();
 336                      }
 337                  }
 338              }
 339          } catch (Exception e) {}
 340          return "unknown";
 341      }
 342  
 343      @Override
 344      protected void onDestroy() {
 345          super.onDestroy();
 346          if (captureSession != null) captureSession.close();
 347          if (camera != null) camera.close();
 348          if (encoder != null) { encoder.stop(); encoder.release(); }
 349          if (server != null) try { server.close(); } catch (IOException e) {}
 350          if (camThread != null) camThread.quitSafely();
 351      }
 352  }
 353