StreamService.java raw

   1  package dev.mleku.h264cam;
   2  
   3  import android.app.*;
   4  import android.content.Intent;
   5  import android.graphics.Bitmap;
   6  import android.graphics.Color;
   7  import android.hardware.camera2.*;
   8  import android.media.MediaCodec;
   9  import android.media.MediaCodecInfo;
  10  import android.media.MediaFormat;
  11  import android.os.*;
  12  import android.util.Log;
  13  import android.view.Surface;
  14  
  15  import java.io.*;
  16  import java.net.*;
  17  import java.nio.ByteBuffer;
  18  import java.util.*;
  19  import java.util.concurrent.CopyOnWriteArrayList;
  20  
  21  public class StreamService extends Service {
  22      static final String TAG = "H264Cam";
  23      static final int PORT = 8080;
  24      static final int WIDTH = 1920;
  25      static final int HEIGHT = 1080;
  26      static final int FPS = 25;
  27      static final int BITRATE = 16_000_000;
  28      static final int NOTIF_ID = 1;
  29      static final String CHANNEL_ID = "h264cam_stream";
  30      static final int AE_INTERVAL_MS = 2000;
  31      static final int AE_TARGET = 120;
  32      static final int AE_DEADZONE = 15;
  33      static final double AE_STEP = 0.02;
  34  
  35      HandlerThread camThread;
  36      Handler camHandler;
  37      CameraDevice camera;
  38      MediaCodec encoder;
  39      Surface encoderSurface;
  40      CameraCaptureSession captureSession;
  41      ServerSocket server;
  42      final CopyOnWriteArrayList<OutputStream> clients = new CopyOnWriteArrayList<>();
  43      byte[] sps, pps;
  44      int maxIso = 6400;
  45      int minIso = 100;
  46      long minExposureNs = 100_000L;
  47      long maxExposureNs = 100_000_000L;
  48      long exposureNs = 500_000L;
  49      int isoValue = 100;
  50      double currentFrac = 0.0;
  51      long manualOverrideUntil = 0;
  52      PowerManager.WakeLock wakeLock;
  53  
  54      Surface previewSurface;
  55      StatusListener statusListener;
  56      LuminanceSampler luminanceSampler;
  57  
  58      interface StatusListener {
  59          void onStatusChanged(String text);
  60          void onExposureChanged(double frac, long exposureNs, int iso);
  61      }
  62  
  63      interface LuminanceSampler {
  64          int sample();
  65      }
  66  
  67      final Runnable autoExposure = new Runnable() {
  68          public void run() {
  69              if (System.currentTimeMillis() < manualOverrideUntil) {
  70                  camHandler.postDelayed(this, AE_INTERVAL_MS);
  71                  return;
  72              }
  73              int lum = luminanceSampler != null ? luminanceSampler.sample() : -1;
  74              if (lum >= 0) {
  75                  int err = lum - AE_TARGET;
  76                  if (Math.abs(err) > AE_DEADZONE) {
  77                      double nudge = err < 0 ? AE_STEP : -AE_STEP;
  78                      currentFrac = Math.max(0.0, Math.min(1.0, currentFrac + nudge));
  79                      exposureFromFrac(currentFrac);
  80                      applyExposure();
  81                      if (statusListener != null)
  82                          statusListener.onExposureChanged(currentFrac, exposureNs, isoValue);
  83                  }
  84              }
  85              camHandler.postDelayed(this, AE_INTERVAL_MS);
  86          }
  87      };
  88  
  89      public class LocalBinder extends Binder {
  90          StreamService getService() { return StreamService.this; }
  91      }
  92      final IBinder binder = new LocalBinder();
  93  
  94      @Override
  95      public IBinder onBind(Intent intent) { return binder; }
  96  
  97      @Override
  98      public void onCreate() {
  99          super.onCreate();
 100          createNotificationChannel();
 101          startForeground(NOTIF_ID, buildNotification("Starting..."));
 102  
 103          PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
 104          wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "H264Cam::Stream");
 105          wakeLock.acquire();
 106  
 107          camThread = new HandlerThread("cam");
 108          camThread.start();
 109          camHandler = new Handler(camThread.getLooper());
 110  
 111          readSensorRanges();
 112          setupEncoder();
 113          startHttpServer();
 114      }
 115  
 116      void readSensorRanges() {
 117          try {
 118              CameraManager mgr = (CameraManager) getSystemService(CAMERA_SERVICE);
 119              CameraCharacteristics chars = mgr.getCameraCharacteristics(mgr.getCameraIdList()[0]);
 120              android.util.Range<Integer> isoRange = chars.get(
 121                  CameraCharacteristics.SENSOR_INFO_SENSITIVITY_RANGE);
 122              if (isoRange != null) {
 123                  minIso = isoRange.getLower();
 124                  maxIso = isoRange.getUpper();
 125              }
 126              android.util.Range<Long> expRange = chars.get(
 127                  CameraCharacteristics.SENSOR_INFO_EXPOSURE_TIME_RANGE);
 128              if (expRange != null) {
 129                  minExposureNs = Math.max(expRange.getLower(), 10_000L);
 130                  maxExposureNs = Math.min(expRange.getUpper(), 100_000_000L);
 131              }
 132          } catch (Exception e) {
 133              Log.e(TAG, "sensor range read failed", e);
 134          }
 135          double logMin = Math.log(minExposureNs);
 136          double logMax = Math.log(maxExposureNs);
 137          currentFrac = (Math.log(exposureNs) - logMin) / (logMax - logMin);
 138      }
 139  
 140      void setupEncoder() {
 141          try {
 142              MediaFormat fmt = MediaFormat.createVideoFormat("video/avc", WIDTH, HEIGHT);
 143              fmt.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE);
 144              fmt.setInteger(MediaFormat.KEY_FRAME_RATE, FPS);
 145              fmt.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1);
 146              fmt.setInteger(MediaFormat.KEY_BITRATE_MODE,
 147                  MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR);
 148              fmt.setInteger(MediaFormat.KEY_PROFILE,
 149                  MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
 150              fmt.setInteger(MediaFormat.KEY_COLOR_FORMAT,
 151                  MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
 152  
 153              encoder = MediaCodec.createEncoderByType("video/avc");
 154              encoder.setCallback(new MediaCodec.Callback() {
 155                  public void onInputBufferAvailable(MediaCodec codec, int index) {}
 156                  public void onOutputBufferAvailable(MediaCodec codec, int index,
 157                          MediaCodec.BufferInfo info) {
 158                      ByteBuffer buf = codec.getOutputBuffer(index);
 159                      if (buf != null && info.size > 0) {
 160                          byte[] data = new byte[info.size];
 161                          buf.get(data);
 162                          broadcast(data);
 163                      }
 164                      codec.releaseOutputBuffer(index, false);
 165                  }
 166                  public void onError(MediaCodec codec, MediaCodec.CodecException e) {
 167                      Log.e(TAG, "encoder error", e);
 168                  }
 169                  public void onOutputFormatChanged(MediaCodec codec, MediaFormat fmt) {
 170                      ByteBuffer spsBuf = fmt.getByteBuffer("csd-0");
 171                      ByteBuffer ppsBuf = fmt.getByteBuffer("csd-1");
 172                      if (spsBuf != null) { sps = new byte[spsBuf.remaining()]; spsBuf.get(sps); }
 173                      if (ppsBuf != null) { pps = new byte[ppsBuf.remaining()]; ppsBuf.get(pps); }
 174                      Log.i(TAG, "got SPS/PPS");
 175                  }
 176              }, camHandler);
 177              encoder.configure(fmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
 178              encoderSurface = encoder.createInputSurface();
 179              encoder.start();
 180          } catch (Exception e) {
 181              Log.e(TAG, "encoder setup failed", e);
 182          }
 183      }
 184  
 185      void openCamera() {
 186          try {
 187              CameraManager mgr = (CameraManager) getSystemService(CAMERA_SERVICE);
 188              String id = mgr.getCameraIdList()[0];
 189              mgr.openCamera(id, new CameraDevice.StateCallback() {
 190                  public void onOpened(CameraDevice cam) {
 191                      camera = cam;
 192                      startCapture();
 193                  }
 194                  public void onDisconnected(CameraDevice cam) {
 195                      Log.w(TAG, "camera disconnected");
 196                      cam.close();
 197                      camera = null;
 198                  }
 199                  public void onError(CameraDevice cam, int err) {
 200                      Log.e(TAG, "camera error: " + err);
 201                      cam.close();
 202                      camera = null;
 203                  }
 204              }, camHandler);
 205          } catch (Exception e) {
 206              Log.e(TAG, "camera open failed", e);
 207          }
 208      }
 209  
 210      void startCapture() {
 211          try {
 212              List<Surface> targets = new ArrayList<>();
 213              targets.add(encoderSurface);
 214              if (previewSurface != null) targets.add(previewSurface);
 215  
 216              camera.createCaptureSession(targets,
 217                  new CameraCaptureSession.StateCallback() {
 218                      public void onConfigured(CameraCaptureSession session) {
 219                          captureSession = session;
 220                          try {
 221                              session.setRepeatingRequest(buildRequest(), null, camHandler);
 222                              camHandler.removeCallbacks(autoExposure);
 223                              camHandler.postDelayed(autoExposure, AE_INTERVAL_MS);
 224                              notifyStatus();
 225                          } catch (Exception e) {
 226                              Log.e(TAG, "capture failed", e);
 227                          }
 228                      }
 229                      public void onConfigureFailed(CameraCaptureSession session) {
 230                          Log.e(TAG, "session config failed");
 231                      }
 232                  }, camHandler);
 233          } catch (Exception e) {
 234              Log.e(TAG, "startCapture failed", e);
 235          }
 236      }
 237  
 238      CaptureRequest buildRequest() throws CameraAccessException {
 239          CaptureRequest.Builder req = camera.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
 240          req.addTarget(encoderSurface);
 241          if (previewSurface != null) req.addTarget(previewSurface);
 242          req.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);
 243          req.set(CaptureRequest.SENSOR_EXPOSURE_TIME, exposureNs);
 244          req.set(CaptureRequest.SENSOR_SENSITIVITY, isoValue);
 245          return req.build();
 246      }
 247  
 248      void exposureFromFrac(double frac) {
 249          double logMin = Math.log(minExposureNs);
 250          double logMax = Math.log(maxExposureNs);
 251          exposureNs = (long) Math.exp(logMin + frac * (logMax - logMin));
 252          double logIsoMin = Math.log(minIso);
 253          double logIsoMax = Math.log(maxIso);
 254          isoValue = (int) Math.exp(logIsoMin + frac * (logIsoMax - logIsoMin));
 255      }
 256  
 257      void applyExposure() {
 258          if (captureSession == null || camera == null) return;
 259          try {
 260              captureSession.setRepeatingRequest(buildRequest(), null, camHandler);
 261          } catch (Exception e) {
 262              Log.e(TAG, "exposure update failed", e);
 263          }
 264      }
 265  
 266      void setExposureFrac(double frac) {
 267          currentFrac = frac;
 268          manualOverrideUntil = System.currentTimeMillis() + 10_000;
 269          exposureFromFrac(frac);
 270          applyExposure();
 271      }
 272  
 273      void setPreviewSurface(Surface surface) {
 274          previewSurface = surface;
 275          if (camera != null) {
 276              // rebuild capture session to include/exclude preview
 277              startCapture();
 278          }
 279      }
 280  
 281      void removePreviewSurface() {
 282          previewSurface = null;
 283          if (camera != null) {
 284              startCapture();
 285          }
 286      }
 287  
 288      void broadcast(byte[] data) {
 289          if (clients.isEmpty()) return;
 290          List<OutputStream> dead = new ArrayList<>();
 291          for (OutputStream out : clients) {
 292              try {
 293                  out.write(data);
 294                  out.flush();
 295              } catch (IOException e) {
 296                  dead.add(out);
 297              }
 298          }
 299          if (!dead.isEmpty()) {
 300              clients.removeAll(dead);
 301              notifyStatus();
 302          }
 303      }
 304  
 305      void startHttpServer() {
 306          new Thread(() -> {
 307              try {
 308                  server = new ServerSocket(PORT);
 309                  Log.i(TAG, "HTTP server on port " + PORT);
 310                  notifyStatus();
 311                  while (!server.isClosed()) {
 312                      Socket sock = server.accept();
 313                      new Thread(() -> handleClient(sock)).start();
 314                  }
 315              } catch (IOException e) {
 316                  Log.e(TAG, "server error", e);
 317              }
 318          }).start();
 319      }
 320  
 321      void handleClient(Socket sock) {
 322          try {
 323              BufferedReader in = new BufferedReader(
 324                  new InputStreamReader(sock.getInputStream()));
 325              String line = in.readLine();
 326              Log.i(TAG, "client: " + sock.getInetAddress() + " " + line);
 327              while ((line = in.readLine()) != null && !line.isEmpty()) {}
 328  
 329              OutputStream out = sock.getOutputStream();
 330              out.write(("HTTP/1.1 200 OK\r\n" +
 331                  "Content-Type: video/h264\r\n" +
 332                  "Connection: close\r\n" +
 333                  "Cache-Control: no-cache\r\n" +
 334                  "\r\n").getBytes());
 335              if (sps != null) out.write(sps);
 336              if (pps != null) out.write(pps);
 337              out.flush();
 338  
 339              clients.add(out);
 340              notifyStatus();
 341  
 342              try {
 343                  while (sock.getInputStream().read() != -1) {}
 344              } catch (IOException e) {}
 345  
 346              clients.remove(out);
 347              notifyStatus();
 348          } catch (IOException e) {}
 349      }
 350  
 351      void notifyStatus() {
 352          String text = statusText();
 353          updateNotification(text);
 354          if (statusListener != null) statusListener.onStatusChanged(text);
 355      }
 356  
 357      String statusText() {
 358          return "http://" + getLocalIp() + ":" + PORT + "/video  " +
 359              WIDTH + "x" + HEIGHT + " " + FPS + "fps H264 " +
 360              (BITRATE / 1_000_000) + "Mbps  " +
 361              clients.size() + " client(s)";
 362      }
 363  
 364      String getLocalIp() {
 365          try {
 366              for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
 367                      en.hasMoreElements();) {
 368                  NetworkInterface intf = en.nextElement();
 369                  for (Enumeration<InetAddress> addrs = intf.getInetAddresses();
 370                          addrs.hasMoreElements();) {
 371                      InetAddress addr = addrs.nextElement();
 372                      if (!addr.isLoopbackAddress() && addr instanceof Inet4Address)
 373                          return addr.getHostAddress();
 374                  }
 375              }
 376          } catch (Exception e) {}
 377          return "unknown";
 378      }
 379  
 380      void createNotificationChannel() {
 381          NotificationChannel ch = new NotificationChannel(
 382              CHANNEL_ID, "Camera Stream", NotificationManager.IMPORTANCE_LOW);
 383          ch.setDescription("Active camera streaming");
 384          getSystemService(NotificationManager.class).createNotificationChannel(ch);
 385      }
 386  
 387      Notification buildNotification(String text) {
 388          Intent intent = new Intent(this, MainActivity.class);
 389          intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
 390          PendingIntent pi = PendingIntent.getActivity(this, 0, intent,
 391              PendingIntent.FLAG_IMMUTABLE);
 392          return new Notification.Builder(this, CHANNEL_ID)
 393              .setContentTitle("H264Cam")
 394              .setContentText(text)
 395              .setSmallIcon(android.R.drawable.ic_menu_camera)
 396              .setContentIntent(pi)
 397              .setOngoing(true)
 398              .build();
 399      }
 400  
 401      void updateNotification(String text) {
 402          getSystemService(NotificationManager.class)
 403              .notify(NOTIF_ID, buildNotification(text));
 404      }
 405  
 406      @Override
 407      public void onDestroy() {
 408          camHandler.removeCallbacks(autoExposure);
 409          if (captureSession != null) captureSession.close();
 410          if (camera != null) camera.close();
 411          if (encoder != null) { encoder.stop(); encoder.release(); }
 412          if (server != null) try { server.close(); } catch (IOException e) {}
 413          if (camThread != null) camThread.quitSafely();
 414          if (wakeLock != null && wakeLock.isHeld()) wakeLock.release();
 415          super.onDestroy();
 416      }
 417  }
 418