package br.carinha.rosto; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.ImageFormat; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.YuvImage; import android.media.Image; import android.util.Log; import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.URL; import java.nio.ByteBuffer; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.ConcurrentLinkedQueue; /** One network worker and one pending JPEG; camera never waits on the network. */ public final class ControleRemoto { private volatile String servidor; private volatile boolean preview; private volatile JSONObject config = new JSONObject(); private volatile byte[] pendente; private volatile ScheduledExecutorService worker; private long consultarEm; private long quadroEm; private long logEm; private final Runnable mudanca; public interface RTCListener { boolean receber(JSONObject pacote); } private RTCListener rtcListener; private final ConcurrentLinkedQueue sinais = new ConcurrentLinkedQueue<>(); private String rtcId = ""; private int rtcDesde; private long rtcConsultarEm; private final ConcurrentLinkedQueue ajustes = new ConcurrentLinkedQueue<>(); public JSONObject config() { return config; } public void configurar(JSONObject parte) { ajustes.offer(parte); } public ControleRemoto(Runnable mudanca) { this.mudanca = mudanca; } public void rtcListener(RTCListener listener) { rtcListener = listener; } public boolean videoRTC() { return config.optBoolean("videoRTC", true); } public void sinalRTC(String json) { if (sinais.size() < 64) sinais.offer(json); } private volatile String token = ""; /** Token do servidor (CARINHA_TOKEN); vai como Authorization em todo pedido. */ public void token(String valor) { token = valor == null ? "" : valor.trim(); } public void servidor(String valor) { servidor = valor; pendente = null; consultarEm = 0; rtcId = ""; rtcDesde = 0; rtcConsultarEm = 0; } public boolean preview() { return preview && worker != null; } public boolean economia() { return config.optBoolean("economia", true); } public float brilho() { return (float) config.optDouble("brilho", 0.7); } public float brilhoRepouso() { return (float) config.optDouble("brilhoRepouso", 0.08); } public long repousoTela() { return (long) (config.optDouble("repousoTela", 30) * 1000); } public double somlim() { return config.optDouble("somlim", 0.35); } public double movlim() { return config.optDouble("movlim", 0.05); } public synchronized void iniciar() { if (worker != null) return; consultarEm = 0; worker = Executors.newSingleThreadScheduledExecutor(); worker.scheduleWithFixedDelay(new Runnable() { @Override public void run() { tick(); } }, 0, 200, TimeUnit.MILLISECONDS); } public synchronized void parar() { if (worker != null) worker.shutdownNow(); worker = null; preview = false; pendente = null; sinais.clear(); } private void tick() { String base = servidor; if (base == null || worker == null) return; HttpURLConnection conn = null; try { long agora = System.currentTimeMillis(); JSONObject ajuste = ajustes.peek(); if (ajuste != null) { conn = abrir(base + "/app/config"); conn.setRequestMethod("POST"); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "application/json"); byte[] dados = ajuste.toString().getBytes("UTF-8"); try (java.io.OutputStream out = conn.getOutputStream()) { out.write(dados); } if (conn.getResponseCode() != 200) throw new java.io.IOException("Ajuste não salvo"); ajustes.poll(); consultarEm = 0; conn.disconnect(); conn = null; } if (agora >= consultarEm) { consultarEm = agora + 1000; conn = abrir(base + "/app/config"); String texto; try (InputStream input = conn.getInputStream()) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int n; while ((n = input.read(buffer)) != -1 && bytes.size() < 16384) bytes.write(buffer, 0, n); texto = bytes.toString("UTF-8"); } conn.disconnect(); conn = null; JSONObject recebido = new JSONObject(texto); boolean novo = recebido.optBoolean("preview", false); if (worker == null || !base.equals(servidor)) return; config = recebido; if (novo != preview) { preview = novo; if (!novo) pendente = null; Log.i("CarinhaPC", "transmissao=" + novo); } mudanca.run(); } if (videoRTC()) rtcTick(base); byte[] jpeg = pendente; pendente = null; if (jpeg != null && preview()) { conn = abrir(base + "/camera/frame"); conn.setRequestMethod("POST"); conn.setDoOutput(true); conn.setRequestProperty("Content-Type", "image/jpeg"); conn.setFixedLengthStreamingMode(jpeg.length); try (java.io.OutputStream out = conn.getOutputStream()) { out.write(jpeg); } int code = conn.getResponseCode(); if (code == 409) { preview = false; pendente = null; } else if (code != 200) throw new java.io.IOException("HTTP " + code); } } catch (Exception e) { pendente = null; if (System.currentTimeMillis() >= logEm) { logEm = System.currentTimeMillis() + 5000; Log.w("CarinhaPC", "servidor indisponivel: " + e); } } finally { if (conn != null) conn.disconnect(); } } private HttpURLConnection abrir(String url) throws Exception { HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection(); c.setConnectTimeout(1500); c.setReadTimeout(1500); c.setUseCaches(false); if (!token.isEmpty()) c.setRequestProperty("Authorization", "Bearer " + token); return c; } public void quadro(Image image, int rotacao) { if (videoRTC()) return; long agora = System.currentTimeMillis(); if (!preview() || pendente != null || agora < quadroEm) return; quadroEm = agora + 200; try { Image.Plane[] p = image.getPlanes(); ByteBuffer[] buffers = {p[0].getBuffer(), p[1].getBuffer(), p[2].getBuffer()}; int[] rows = {p[0].getRowStride(), p[1].getRowStride(), p[2].getRowStride()}; int[] pixels = {p[0].getPixelStride(), p[1].getPixelStride(), p[2].getPixelStride()}; int w = image.getWidth(), h = image.getHeight(); byte[] nv21 = YuvFrame.nv21(w, h, buffers, rows, pixels); ByteArrayOutputStream bytes = new ByteArrayOutputStream(); new YuvImage(nv21, ImageFormat.NV21, w, h, null) .compressToJpeg(new Rect(0, 0, w, h), 70, bytes); byte[] jpeg = bytes.toByteArray(); if (rotacao != 0) { Bitmap b = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length); Matrix m = new Matrix(); m.postRotate(rotacao); Bitmap rotated = Bitmap.createBitmap(b, 0, 0, b.getWidth(), b.getHeight(), m, true); bytes.reset(); rotated.compress(Bitmap.CompressFormat.JPEG, 70, bytes); jpeg = bytes.toByteArray(); if (rotated != b) rotated.recycle(); b.recycle(); } if (preview()) pendente = jpeg; } catch (Exception e) { Log.w("CarinhaPC", "quadro descartado: " + e); } } private void rtcTick(String base) throws Exception { String json; while ((json = sinais.poll()) != null) { HttpURLConnection c = abrir(base + "/rtc/send"); try { byte[] body = json.getBytes("UTF-8"); c.setRequestMethod("POST"); c.setDoOutput(true); c.setRequestProperty("Content-Type", "application/json"); c.setFixedLengthStreamingMode(body.length); try (java.io.OutputStream out = c.getOutputStream()) { out.write(body); } int code = c.getResponseCode(); if (code != 200 && code != 409) throw new java.io.IOException("RTC HTTP " + code); } finally { c.disconnect(); } } long agora = System.currentTimeMillis(); if (agora < rtcConsultarEm || rtcListener == null) return; rtcConsultarEm = agora + 400; HttpURLConnection c = abrir(base + "/rtc/poll?lado=app&desde=" + rtcDesde + "&id=" + rtcId); try { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); try (InputStream in = c.getInputStream()) { byte[] buffer = new byte[4096]; int n; while ((n = in.read(buffer)) != -1 && bytes.size() < 262144) bytes.write(buffer, 0, n); } JSONObject packet = new JSONObject(bytes.toString("UTF-8")); if (worker == null || !base.equals(servidor)) return; if (rtcListener.receber(packet)) { String id = packet.optString("id", ""); if (!id.equals(rtcId)) { rtcDesde = 0; rtcId = id; } org.json.JSONArray messages = packet.optJSONArray("mensagens"); if (messages != null) for (int i = 0; i < messages.length(); i++) rtcDesde = Math.max(rtcDesde, messages.getJSONObject(i).getInt("seq")); } } finally { c.disconnect(); } } }