| app/src/main/AndroidManifest.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/anyun/h264/FileLoggingTree.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/anyun/h264/H264EncodeService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/anyun/h264/H264EncodeService2.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/anyun/h264/H264Encoder.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/anyun/h264/MainActivity.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| 多进程方案使用说明.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
app/src/main/AndroidManifest.xml
@@ -37,7 +37,7 @@ </intent-filter> </activity> <!-- H264ç¼ç æå¡ --> <!-- H264ç¼ç æå¡ï¼ç¬¬ä¸ä¸ªæå头ï¼ä¸»è¿ç¨ï¼ --> <service android:name=".H264EncodeService" android:enabled="true" @@ -46,6 +46,17 @@ <action android:name="com.anyun.h264.H264EncodeService" /> </intent-filter> </service> <!-- H264ç¼ç æå¡2ï¼ç¬¬äºä¸ªæå头ï¼ç¬ç«è¿ç¨ï¼ --> <service android:name=".H264EncodeService2" android:enabled="true" android:exported="true" android:process=":camera2"> <intent-filter> <action android:name="com.anyun.h264.H264EncodeService2" /> </intent-filter> </service> </application> </manifest> app/src/main/java/com/anyun/h264/FileLoggingTree.java
@@ -5,11 +5,13 @@ import timber.log.Timber; import java.io.File; import java.io.FileFilter; import java.io.FileWriter; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; /** * Timber Tree implementation that logs to files. @@ -19,6 +21,8 @@ private static final String LOG_DIR = "nvlog"; private static final String LOG_PREFIX = "h264_"; private static final String LOG_SUFFIX = ".log"; // æ¥å¿æä»¶ä¿çå¤©æ° private static final int LOG_RETENTION_DAYS = 3; private static final String DATE_FORMAT = "yyyyMMdd"; private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; @@ -36,6 +40,8 @@ // Check if we need to update the log file (new day) if (!logFileName.equals(currentLogFile)) { currentLogFile = logFileName; // æ°çä¸å¤©ï¼é¡ºä¾¿æ¸ çè¿ææ¥å¿æä»¶ cleanupExpiredLogFiles(LOG_RETENTION_DAYS); } File logFile = getLogFile(logFileName); @@ -88,6 +94,50 @@ } catch (IOException e) { Log.e("FileLoggingTree", "Error getting log file", e); return null; } } /** * æ¸ çè¶ åºä¿ç天æ°çæ¥å¿æä»¶ */ private void cleanupExpiredLogFiles(int retentionDays) { if (retentionDays <= 0) { return; } try { File logDir = new File(Environment.getExternalStorageDirectory(), LOG_DIR); if (!logDir.exists() || !logDir.isDirectory()) { return; } long retentionMillis = TimeUnit.DAYS.toMillis(Math.max(1, retentionDays)); long cutoffTime = System.currentTimeMillis() - retentionMillis; File[] files = logDir.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { String name = pathname.getName(); return name.startsWith(LOG_PREFIX) && name.endsWith(LOG_SUFFIX); } }); if (files == null || files.length == 0) { return; } for (File file : files) { if (file.lastModified() < cutoffTime) { boolean deleted = file.delete(); if (deleted) { Log.i("FileLoggingTree", "Deleted expired log file: " + file.getAbsolutePath()); } else { Log.w("FileLoggingTree", "Failed to delete expired log file: " + file.getAbsolutePath()); } } } } catch (Exception e) { Log.e("FileLoggingTree", "Error cleaning up expired log files", e); } } @@ -144,3 +194,4 @@ } } app/src/main/java/com/anyun/h264/H264EncodeService.java
@@ -1,7 +1,10 @@ package com.anyun.h264; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.RemoteException; import timber.log.Timber; @@ -19,6 +22,8 @@ import java.util.Date; import java.util.List; import java.util.Locale; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * H264ç¼ç æå¡ @@ -31,6 +36,12 @@ private H264FileTransmitter h264FileTransmitter; // H264æä»¶ä¼ è¾å¨ private String outputFileDirectory; // H264æä»¶è¾åºç®å½ private WatermarkInfo currentWatermarkInfo; // å½åæ°´å°ä¿¡æ¯ private static final int H264_FILE_RETENTION_DAYS = 5; // 坿 ¹æ®éæ±è°æ´ä¸º3æ5天 // å¤è¿ç¨æ¯æï¼ç¬¬äºä¸ªæå头çæå¡è¿æ¥ private IH264EncodeService camera2Service; private ServiceConnection camera2Connection; private boolean isCamera2Bound = false; // é»è®¤ç¼ç åæ° private static final int DEFAULT_WIDTH = 640; @@ -68,6 +79,9 @@ // åå§åè¾åºæä»¶ç®å½ï¼ä½¿ç¨åºç¨å¤é¨åå¨ç®å½ï¼ outputFileDirectory = getExternalFilesDir(null).getAbsolutePath(); Timber.d("Output file directory: %s", outputFileDirectory); // æ¸ çè¿æçH264æä»¶ cleanupExpiredH264Files(H264_FILE_RETENTION_DAYS); } @Override @@ -91,6 +105,18 @@ // 忢并鿾ç¼ç å¨åæä»¶ä¼ è¾å¨ stopEncoder(); stopFileTransmitter(); // è§£ç»ç¬¬äºä¸ªè¿ç¨çæå¡ if (isCamera2Bound && camera2Connection != null) { try { unbindService(camera2Connection); } catch (Exception e) { Timber.e(e, "Error unbinding camera2 service"); } isCamera2Bound = false; camera2Service = null; camera2Connection = null; } } /** @@ -103,6 +129,7 @@ int height; int framerate; String simPhone; Integer cameraId; // æå头IDï¼1æ2ï¼ç¨äºå¤è¿ç¨æ¹æ¡ï¼ // ä»JSONè§£æé ç½® static EncodeConfig fromJson(String jsonConfig) throws JSONException { @@ -115,6 +142,7 @@ config.ip = null; config.port = 0; config.simPhone = null; config.cameraId = 1; // é»è®¤ä½¿ç¨ç¬¬ä¸ä¸ªæå头 return config; } @@ -125,6 +153,13 @@ config.ip = json.optString("ip", null); config.port = json.optInt("port", 0); config.simPhone = json.optString("simPhone", null); // è§£æcameraIdï¼å¦ææªæå®ï¼é»è®¤ä¸º1ï¼ if (json.has("cameraId")) { config.cameraId = json.optInt("cameraId", 1); } else { config.cameraId = 1; // é»è®¤ä½¿ç¨ç¬¬ä¸ä¸ªæå头 } return config; } @@ -171,15 +206,34 @@ * 4-å¼å§ä¼ è¾H264æä»¶ï¼ä»æä»¶è¯»åå¹¶ç½ç»æ¨éï¼ï¼ * 5-忢H264æä»¶ä¼ è¾ * @param jsonConfig JSONæ ¼å¼çé ç½®åæ° * action 0/2: å å«ï¼ipãportãwidthãheightãframerateãsimPhone * action 0/2: å å«ï¼ipãportãwidthãheightãframerateãsimPhoneãcameraIdï¼å¯éï¼1æ2ï¼é»è®¤1ï¼ * action 4: å å«ï¼ipãportãframerateãsimPhoneãfilePathãprotocolTypeï¼å¯éï¼1-UDPï¼2-TCPï¼é»è®¤TCPï¼ * action 1/3/5: æ¤åæ°å¯ä¸ºç©ºænull * action 1/3/5: æ¤åæ°å¯ä¸ºç©ºænullï¼æå å«cameraIdæ¥æå®è¦åæ¢çæå头 * @return 0-æåï¼1-失败 */ private synchronized int controlEncode(int action, String jsonConfig) { Timber.d("controlEncode called with action: %d, jsonConfig: %s", action, jsonConfig); try { // è§£æcameraIdï¼å¦æé ç½®ä¸æï¼ Integer cameraId = null; if (jsonConfig != null && !jsonConfig.trim().isEmpty()) { try { JSONObject json = new JSONObject(jsonConfig); if (json.has("cameraId")) { cameraId = json.optInt("cameraId", 1); } } catch (JSONException e) { // 忽ç¥è§£æé误ï¼ç»§ç»ä½¿ç¨å½åè¿ç¨ } } // 妿æå®äºcameraId=2ï¼è·¯ç±å°ç¬¬äºä¸ªè¿ç¨ if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } // å¦åå¨å½åè¿ç¨ï¼cameraId=1ï¼å¤ç switch (action) { case 0: // å¼å¯h264æä»¶åå ¥ try { @@ -191,6 +245,10 @@ } case 1: // 忢h264ç¼ç 并忢åå ¥æä»¶ // æ£æ¥æ¯å¦æå®äºcameraId=2 if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } return stopEncoder(); case 2: // å¼å¯ç½ç»æ¨éh264ï¼ä¸åå ¥æä»¶ï¼ @@ -203,6 +261,10 @@ } case 3: // 忢h264ç¼ç 并忢ç½ç»æ¨é // æ£æ¥æ¯å¦æå®äºcameraId=2 if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } return stopEncoder(); case 4: // å¼å§ä¼ è¾H264æä»¶ @@ -216,6 +278,10 @@ case 5: // 忢H264æä»¶ä¼ è¾ Timber.i("客æ·ç«¯è¯·æ±åæ¢è§é¢æä»¶ä¸ä¼ "); // æ£æ¥æ¯å¦æå®äºcameraId=2 if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } return stopFileTransmitter(); default: @@ -226,6 +292,85 @@ Timber.e(e, "Error in controlEncode"); return 1; // 失败 } } /** * å¨ç¬¬äºä¸ªè¿ç¨ï¼camera2ï¼ä¸æ§è¡ç¼ç æ§å¶ */ private int controlEncodeInProcess2(int action, String jsonConfig) { Timber.d("Routing to process 2 (camera2) for action: %d", action); try { // ç¡®ä¿ç¬¬äºä¸ªè¿ç¨çæå¡å·²ç»å® if (!ensureCamera2ServiceBound()) { Timber.e("Failed to bind camera2 service"); return 1; } // è°ç¨ç¬¬äºä¸ªè¿ç¨çæå¡ if (camera2Service != null) { return camera2Service.controlEncode(action, jsonConfig); } else { Timber.e("Camera2 service is null"); return 1; } } catch (RemoteException e) { Timber.e(e, "Error calling camera2 service"); return 1; } } /** * ç¡®ä¿ç¬¬äºä¸ªè¿ç¨çæå¡å·²ç»å® */ private synchronized boolean ensureCamera2ServiceBound() { if (isCamera2Bound && camera2Service != null) { return true; } Timber.d("Binding to camera2 service..."); final CountDownLatch latch = new CountDownLatch(1); final boolean[] success = {false}; camera2Connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Timber.d("Camera2 service connected"); camera2Service = IH264EncodeService.Stub.asInterface(service); isCamera2Bound = true; success[0] = true; latch.countDown(); } @Override public void onServiceDisconnected(ComponentName name) { Timber.w("Camera2 service disconnected"); camera2Service = null; isCamera2Bound = false; } }; Intent intent = new Intent(this, H264EncodeService2.class); boolean bound = bindService(intent, camera2Connection, Context.BIND_AUTO_CREATE); if (!bound) { Timber.e("Failed to bind camera2 service"); return false; } // çå¾ æå¡è¿æ¥ï¼æå¤5ç§ï¼ try { if (!latch.await(5, TimeUnit.SECONDS)) { Timber.e("Timeout waiting for camera2 service connection"); return false; } } catch (InterruptedException e) { Timber.e(e, "Interrupted while waiting for camera2 service"); return false; } return success[0]; } /** @@ -245,35 +390,37 @@ h264Encoder = new H264Encoder(); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ int width = config != null ? config.width : DEFAULT_WIDTH; int height = config != null ? config.height : DEFAULT_HEIGHT; int framerate = config != null ? config.framerate : DEFAULT_FRAME_RATE; int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH; int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT; int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE; h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); long timeFile = System.currentTimeMillis()/1000*1000;//Dateæ¯ç§ï¼æä»¥ä¸ºäºè·ä¸åçDate starttimeä¸è´ï¼æ¤å¤é¤ä»¥1000 ç§ SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss"); String str = bcdFormat.format(timeFile); Timber.i("æä»¶åï¼%s", str); // 设置è¾åºæä»¶ String fileName = "h264_" + timeFile+ ".h264"; File outputFile = new File(outputFileDirectory, fileName); h264Encoder.setOutputFile(outputFile.getAbsolutePath()); // 设置è¾åºæä»¶ç®å½ï¼H264Encoderä¼èªå¨ç®¡çæä»¶åå»ºï¼æ¯åéä¸ä¸ªæä»¶ï¼ // 使ç¨ä¸ä¸ªä¸´æ¶æä»¶åæ¥è®¾ç½®ç®å½ï¼H264Encoderä¼å¨åå§åæ¶å建第ä¸ä¸ªæä»¶ File tempFile = new File(outputFileDirectory, "temp.h264"); h264Encoder.setOutputFile(tempFile.getAbsolutePath()); h264Encoder.setEnableFileOutput(true); // å¯ç¨æä»¶è¾åº // ç¦ç¨ç½ç»ä¼ è¾ h264Encoder.setEnableNetworkTransmission(false); // åå§åå¹¶å¯å¨ï¼ä½¿ç¨é ç½®ä¸çå辨çï¼ // æ ¹æ®cameraIdéæ©æå头èå´ int[] cameraIdRange = DEFAULT_CAMERA_ID_RANGE; if (config != null && config.cameraId != null) { // 妿æå®äºcameraIdï¼ä½¿ç¨å¯¹åºçæå头 cameraIdRange = new int[]{config.cameraId, config.cameraId}; } int[] resolution = {width, height}; if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) { if (h264Encoder.initialize(cameraIdRange, null, resolution, false)) { // åºç¨å·²ä¿åçæ°´å°ä¿¡æ¯ï¼å¦ææï¼ if (currentWatermarkInfo != null) { h264Encoder.setWatermarkInfo(currentWatermarkInfo); Timber.d("Applied saved watermark info to encoder"); } h264Encoder.start(); Timber.d("File encode started successfully, output file: %s, resolution: %dx%d, framerate: %d", outputFile.getAbsolutePath(), width, height, framerate); Timber.d("File encode started successfully, output directory: %s, resolution: %dx%d, framerate: %d", outputFileDirectory, width, height, framerate); return 0; // æå } else { Timber.e("Failed to initialize encoder"); @@ -310,22 +457,15 @@ h264Encoder = new H264Encoder(); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ int width = DEFAULT_WIDTH; int height = DEFAULT_HEIGHT; int framerate = DEFAULT_FRAME_RATE; int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH; int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT; int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE; h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); long timeFile = System.currentTimeMillis()/1000*1000; SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss"); String str = bcdFormat.format(timeFile); Timber.i("startNetworkEncode æä»¶åï¼%s", str); // 设置è¾åºæä»¶ String fileName = "h264_" + timeFile+ ".h264"; File outputFile = new File(outputFileDirectory, fileName); h264Encoder.setOutputFile(outputFile.getAbsolutePath()); // 设置è¾åºæä»¶ç®å½ï¼H264Encoderä¼èªå¨ç®¡çæä»¶åå»ºï¼æ¯åéä¸ä¸ªæä»¶ï¼ // 使ç¨ä¸ä¸ªä¸´æ¶æä»¶åæ¥è®¾ç½®ç®å½ï¼H264Encoderä¼å¨åå§åæ¶å建第ä¸ä¸ªæä»¶ File tempFile = new File(outputFileDirectory, "temp.h264"); h264Encoder.setOutputFile(tempFile.getAbsolutePath()); h264Encoder.setEnableFileOutput(true); // å¯ç¨æä»¶è¾åº @@ -339,8 +479,14 @@ h264Encoder.setProtocolParams(simPhone, (byte)1); // åå§åå¹¶å¯å¨ï¼ä½¿ç¨é ç½®ä¸çå辨çï¼ // æ ¹æ®cameraIdéæ©æå头èå´ int[] cameraIdRange = DEFAULT_CAMERA_ID_RANGE; if (config != null && config.cameraId != null) { // 妿æå®äºcameraIdï¼ä½¿ç¨å¯¹åºçæå头 cameraIdRange = new int[]{config.cameraId, config.cameraId}; } int[] resolution = {width, height}; if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) { if (h264Encoder.initialize(cameraIdRange, null, resolution, false)) { // åºç¨å·²ä¿åçæ°´å°ä¿¡æ¯ï¼å¦ææï¼ if (currentWatermarkInfo != null) { h264Encoder.setWatermarkInfo(currentWatermarkInfo); @@ -651,6 +797,41 @@ currentWatermarkInfo = null; } } /** * å é¤è¶ è¿ä¿çæçH264æä»¶ */ private void cleanupExpiredH264Files(int retentionDays) { if (outputFileDirectory == null) { Timber.w("cleanupExpiredH264Files: outputFileDirectory is null"); return; } File dir = new File(outputFileDirectory); if (!dir.exists() || !dir.isDirectory()) { Timber.w("cleanupExpiredH264Files: directory invalid -> %s", outputFileDirectory); return; } long retentionMillis = TimeUnit.DAYS.toMillis(Math.max(1, retentionDays)); long cutoffTime = System.currentTimeMillis() - retentionMillis; File[] files = dir.listFiles((d, name) -> name.toLowerCase(Locale.CHINA).endsWith(".h264")); if (files == null || files.length == 0) { return; } for (File file : files) { if (file.lastModified() < cutoffTime) { boolean deleted = file.delete(); if (deleted) { Timber.i("Deleted expired H264 file: %s", file.getAbsolutePath()); } else { Timber.w("Failed to delete expired H264 file: %s", file.getAbsolutePath()); } } } } /** * 仿件åå»ºèµæºä¿¡æ¯ï¼å¦ææä»¶å¨æ¶é´èå´å ï¼ app/src/main/java/com/anyun/h264/H264EncodeService2.java
New file @@ -0,0 +1,712 @@ package com.anyun.h264; import android.app.Service; import android.content.Intent; import android.os.IBinder; import android.os.RemoteException; import timber.log.Timber; import com.anyun.h264.model.ResourceInfo; import com.anyun.h264.model.WatermarkInfo; import org.json.JSONException; import org.json.JSONObject; import java.io.File; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; /** * H264ç¼ç æå¡2ï¼ç¬¬äºä¸ªæå头ï¼è¿è¡å¨ç¬ç«è¿ç¨ï¼ * æä¾AIDLæ¥å£ä¾å®¢æ·ç«¯è°ç¨ï¼ç¨äºæ§å¶ç¬¬äºä¸ªUSBæå头çH264ç¼ç */ public class H264EncodeService2 extends Service { private static final String TAG = "H264EncodeService2"; private H264Encoder h264Encoder; private H264FileTransmitter h264FileTransmitter; // H264æä»¶ä¼ è¾å¨ private String outputFileDirectory; // H264æä»¶è¾åºç®å½ private WatermarkInfo currentWatermarkInfo; // å½åæ°´å°ä¿¡æ¯ // é»è®¤ç¼ç åæ° private static final int DEFAULT_WIDTH = 640; private static final int DEFAULT_HEIGHT = 480; private static final int DEFAULT_FRAME_RATE = 25; private static final int DEFAULT_BITRATE = 2000000; // 2Mbps // 第äºä¸ªæå头åºå®ä½¿ç¨cameraId=2 private static final int[] CAMERA2_ID_RANGE = {2, 3}; // AIDLæ¥å£å®ç° private final IH264EncodeService.Stub binder = new IH264EncodeService.Stub() { @Override public int controlEncode(int action, String jsonConfig) throws RemoteException { return H264EncodeService2.this.controlEncode(action, jsonConfig); } @Override public List<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException { return H264EncodeService2.this.getResourceList(startTime, endTime); } @Override public void setWatermarkInfo(String watermarkInfo) throws RemoteException { H264EncodeService2.this.setWatermarkInfo(watermarkInfo); } }; @Override public void onCreate() { super.onCreate(); Timber.d("H264EncodeService2 created (process 2 for camera 2)"); // åå§åè¾åºæä»¶ç®å½ï¼ä½¿ç¨åºç¨å¤é¨åå¨ç®å½ï¼ outputFileDirectory = getExternalFilesDir(null).getAbsolutePath(); Timber.d("Output file directory: %s", outputFileDirectory); } @Override public IBinder onBind(Intent intent) { Timber.d("Service2 bound"); return binder; } @Override public boolean onUnbind(Intent intent) { Timber.d("Service2 unbound"); // ä¸èªå¨åæ¢ç¼ç å¨ï¼è®©å®å¨æå¡ä¸ä¿æè¿è¡ return super.onUnbind(intent); } @Override public void onDestroy() { super.onDestroy(); Timber.d("Service2 destroyed"); // 忢并鿾ç¼ç å¨åæä»¶ä¼ è¾å¨ stopEncoder(); stopFileTransmitter(); } /** * ç¼ç é 置类 */ private static class EncodeConfig { String ip; int port; int width; int height; int framerate; String simPhone; // ä»JSONè§£æé ç½® static EncodeConfig fromJson(String jsonConfig) throws JSONException { EncodeConfig config = new EncodeConfig(); if (jsonConfig == null || jsonConfig.trim().isEmpty()) { // 使ç¨é»è®¤å¼ config.width = DEFAULT_WIDTH; config.height = DEFAULT_HEIGHT; config.framerate = DEFAULT_FRAME_RATE; config.ip = null; config.port = 0; config.simPhone = null; return config; } JSONObject json = new JSONObject(jsonConfig); config.width = json.optInt("width", DEFAULT_WIDTH); config.height = json.optInt("height", DEFAULT_HEIGHT); config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE); config.ip = json.optString("ip", null); config.port = json.optInt("port", 0); config.simPhone = json.optString("simPhone", null); return config; } } /** * æä»¶ä¼ è¾é 置类 */ private static class FileTransmitConfig { String ip; int port; int framerate; String simPhone; String filePath; // H264æä»¶è·¯å¾ int protocolType; // å议类åï¼1-UDPï¼2-TCP // ä»JSONè§£æé ç½® static FileTransmitConfig fromJson(String jsonConfig) throws JSONException { FileTransmitConfig config = new FileTransmitConfig(); if (jsonConfig == null || jsonConfig.trim().isEmpty()) { throw new JSONException("File transmit config cannot be empty"); } JSONObject json = new JSONObject(jsonConfig); config.ip = json.optString("ip", null); config.port = json.optInt("port", 0); config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE); config.simPhone = json.optString("simPhone", "013120122580"); config.filePath = json.optString("filePath", null); // å议类åï¼é»è®¤TCPï¼2ï¼ï¼1-UDPï¼2-TCP config.protocolType = json.optInt("protocolType", JT1076ProtocolHelper.PROTOCOL_TYPE_TCP); return config; } } /** * æ§å¶H264ç¼ç åæä»¶ä¼ è¾ï¼ç¬¬äºä¸ªæåå¤´ï¼ */ private synchronized int controlEncode(int action, String jsonConfig) { Timber.d("controlEncode (camera2) called with action: %d, jsonConfig: %s", action, jsonConfig); try { switch (action) { case 0: // å¼å¯h264æä»¶åå ¥ try { EncodeConfig config0 = EncodeConfig.fromJson(jsonConfig); return startFileEncode(config0); } catch (JSONException e) { Timber.e(e, "Failed to parse JSON config: %s", jsonConfig); return 1; } case 1: // 忢h264ç¼ç 并忢åå ¥æä»¶ return stopEncoder(); case 2: // å¼å¯ç½ç»æ¨éh264ï¼ä¸åå ¥æä»¶ï¼ try { EncodeConfig config2 = EncodeConfig.fromJson(jsonConfig); return startNetworkEncode(config2); } catch (JSONException e) { Timber.e(e, "Failed to parse JSON config: %s", jsonConfig); return 1; } case 3: // 忢h264ç¼ç 并忢ç½ç»æ¨é return stopEncoder(); case 4: // å¼å§ä¼ è¾H264æä»¶ try { FileTransmitConfig config4 = FileTransmitConfig.fromJson(jsonConfig); return startFileTransmit(config4); } catch (JSONException e) { Timber.e(e, "Failed to parse JSON config: %s", jsonConfig); return 1; } case 5: // 忢H264æä»¶ä¼ è¾ Timber.i("客æ·ç«¯è¯·æ±åæ¢è§é¢æä»¶ä¸ä¼ (camera2)"); return stopFileTransmitter(); default: Timber.e("Unknown action: %d", action); return 1; // 失败 } } catch (Exception e) { Timber.e(e, "Error in controlEncode (camera2)"); return 1; // 失败 } } /** * å¯å¨æä»¶ç¼ç 模å¼ï¼åªåå ¥æä»¶ï¼ä¸è¿è¡ç½ç»æ¨éï¼ */ private int startFileEncode(EncodeConfig config) { Timber.d("Starting file encode mode (camera2)"); // 妿ç¼ç å¨å·²ç»å¨è¿è¡ï¼å 忢 if (h264Encoder != null) { Timber.w("Encoder is already running (camera2), stopping it first"); stopEncoder(); } try { // å建ç¼ç å¨ h264Encoder = new H264Encoder(); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH; int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT; int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE; h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); // 设置è¾åºæä»¶ç®å½ï¼H264Encoderä¼èªå¨ç®¡çæä»¶åå»ºï¼æ¯åéä¸ä¸ªæä»¶ï¼ // 使ç¨ä¸ä¸ªä¸´æ¶æä»¶åæ¥è®¾ç½®ç®å½ï¼H264Encoderä¼å¨åå§åæ¶å建第ä¸ä¸ªæä»¶ File tempFile = new File(outputFileDirectory, "temp.h264"); h264Encoder.setOutputFile(tempFile.getAbsolutePath()); h264Encoder.setEnableFileOutput(true); // å¯ç¨æä»¶è¾åº // ç¦ç¨ç½ç»ä¼ è¾ h264Encoder.setEnableNetworkTransmission(false); // åå§åå¹¶å¯å¨ï¼ä½¿ç¨ç¬¬äºä¸ªæå头ï¼cameraId=2ï¼ int[] resolution = {width, height}; if (h264Encoder.initialize(CAMERA2_ID_RANGE, null, resolution, false)) { // åºç¨å·²ä¿åçæ°´å°ä¿¡æ¯ï¼å¦ææï¼ if (currentWatermarkInfo != null) { h264Encoder.setWatermarkInfo(currentWatermarkInfo); Timber.d("Applied saved watermark info to encoder (camera2)"); } h264Encoder.start(); Timber.d("File encode started successfully (camera2), output directory: %s, resolution: %dx%d, framerate: %d", outputFileDirectory, width, height, framerate); return 0; // æå } else { Timber.e("Failed to initialize encoder (camera2)"); h264Encoder = null; return 1; // 失败 } } catch (Exception e) { Timber.e(e, "Failed to start file encode (camera2)"); h264Encoder = null; return 1; // 失败 } } /** * å¯å¨ç½ç»æ¨é模å¼ï¼åªè¿è¡ç½ç»æ¨éï¼ä¸åå ¥æä»¶ï¼ */ private int startNetworkEncode(EncodeConfig config) { Timber.d("Starting network encode mode (camera2)"); // 妿ç¼ç å¨å·²ç»å¨è¿è¡ï¼å 忢 if (h264Encoder != null) { Timber.w("Encoder is already running (camera2), stopping it first"); stopEncoder(); } // æ£æ¥å¿ éçé ç½®åæ° if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) { Timber.e("Network encode requires valid ip and port in config (camera2)"); return 1; // 失败 } try { // å建ç¼ç å¨ h264Encoder = new H264Encoder(); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH; int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT; int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE; h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); // 设置è¾åºæä»¶ç®å½ï¼H264Encoderä¼èªå¨ç®¡çæä»¶åå»ºï¼æ¯åéä¸ä¸ªæä»¶ï¼ // 使ç¨ä¸ä¸ªä¸´æ¶æä»¶åæ¥è®¾ç½®ç®å½ï¼H264Encoderä¼å¨åå§åæ¶å建第ä¸ä¸ªæä»¶ File tempFile = new File(outputFileDirectory, "temp.h264"); h264Encoder.setOutputFile(tempFile.getAbsolutePath()); h264Encoder.setEnableFileOutput(true); // å¯ç¨æä»¶è¾åº // å¯ç¨ç½ç»ä¼ è¾å¹¶è®¾ç½®æå¡å¨å°å h264Encoder.setEnableNetworkTransmission(true); h264Encoder.setServerAddress(config.ip, config.port); // 设置åè®®åæ°ï¼ä½¿ç¨é ç½®ä¸çsimPhoneï¼å¦ææªæä¾å使ç¨é»è®¤å¼ï¼ String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty() ? config.simPhone : "013120122580"; h264Encoder.setProtocolParams(simPhone, (byte)2); // 第äºä¸ªæå头使ç¨channelId=2 // åå§åå¹¶å¯å¨ï¼ä½¿ç¨ç¬¬äºä¸ªæå头ï¼cameraId=2ï¼ int[] resolution = {width, height}; if (h264Encoder.initialize(CAMERA2_ID_RANGE, null, resolution, false)) { // åºç¨å·²ä¿åçæ°´å°ä¿¡æ¯ï¼å¦ææï¼ if (currentWatermarkInfo != null) { h264Encoder.setWatermarkInfo(currentWatermarkInfo); Timber.d("Applied saved watermark info to encoder (camera2)"); } h264Encoder.start(); Timber.d("Network encode started successfully (camera2), server: %s:%d, resolution: %dx%d, framerate: %d", config.ip, config.port, width, height, framerate); return 0; // æå } else { Timber.e("Failed to initialize encoder (camera2)"); h264Encoder = null; return 1; // 失败 } } catch (Exception e) { Timber.e(e, "Failed to start network encode (camera2)"); h264Encoder = null; return 1; // 失败 } } /** * 忢ç¼ç å¨ */ private int stopEncoder() { Timber.d("Stopping encoder (camera2)"); if (h264Encoder != null) { try { h264Encoder.stop(); h264Encoder.release(); h264Encoder = null; Timber.d("Encoder stopped successfully (camera2)"); return 0; // æå } catch (Exception e) { Timber.e(e, "Error stopping encoder (camera2)"); h264Encoder = null; return 1; // 失败 } } else { Timber.w("Encoder is not running (camera2)"); return 0; // æåï¼æ²¡æè¿è¡çç¼ç å¨ï¼è§ä¸ºæåï¼ } } /** * å¯å¨æä»¶ä¼ è¾æ¨¡å¼ï¼ä»H264æä»¶è¯»åå¹¶ç½ç»æ¨éï¼ */ private int startFileTransmit(FileTransmitConfig config) { Timber.d("Starting file transmit mode (camera2)"); // 妿æä»¶ä¼ è¾å¨å·²ç»å¨è¿è¡ï¼å 忢 if (h264FileTransmitter != null) { Timber.w("File transmitter is already running (camera2), stopping it first"); stopFileTransmitter(); } // æ£æ¥å¿ éçé ç½®åæ° if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) { Timber.e("File transmit requires valid ip and port in config (camera2)"); return 1; // 失败 } if (config.filePath == null || config.filePath.trim().isEmpty()) { Timber.e("File transmit requires valid filePath in config (camera2)"); return 1; // 失败 } try { // æ£æ¥æä»¶æ¯å¦åå¨ File file = new File(config.filePath); if (!file.exists() || !file.isFile()) { Timber.e("File does not exist: %s (camera2)", config.filePath); return 1; // 失败 } // å建æä»¶ä¼ è¾å¨ h264FileTransmitter = new H264FileTransmitter(); // 设置æå¡å¨å°å h264FileTransmitter.setServerAddress(config.ip, config.port); // 设置å议类å h264FileTransmitter.setProtocolType(1); //1-tcp // 设置åè®®åæ°ï¼SIMå¡å·åé»è¾ééå·ï¼ç¬¬äºä¸ªæå头使ç¨channelId=2ï¼ String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty() ? config.simPhone : "013120122580"; h264FileTransmitter.setProtocolParams(simPhone, (byte)2); // 设置帧çï¼ç¨äºè®¡ç®æ¶é´æ³é´éï¼ int framerate = config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE; h264FileTransmitter.setFrameRate(framerate); // 设置è¿åº¦åè°ï¼å¯éï¼ç¨äºæ¥å¿è¾åºï¼ h264FileTransmitter.setOnTransmitProgressCallback(new H264FileTransmitter.OnTransmitProgressCallback() { @Override public void onProgress(int currentFrame, int totalFrames) { Timber.d("File transmit progress (camera2): frame %d%s", currentFrame, totalFrames > 0 ? " of " + totalFrames : ""); } @Override public void onComplete() { Timber.d("File transmit completed (camera2)"); stopFileTransmitter(); } @Override public void onError(String error) { Timber.e("File transmit error (camera2): %s", error); } }); // åå§åSocketè¿æ¥ if (!h264FileTransmitter.initialize()) { Timber.e("Failed to initialize file transmitter socket (camera2)"); h264FileTransmitter = null; return 1; // 失败 } // å¼å§ä¼ è¾æä»¶ h264FileTransmitter.transmitFile(config.filePath); Timber.d("File transmit started successfully (camera2), file: %s, server: %s:%d, protocol: %s, framerate: %d", config.filePath, config.ip, config.port, config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP", framerate); return 0; // æå } catch (Exception e) { Timber.e(e, "Failed to start file transmit (camera2)"); if (h264FileTransmitter != null) { try { h264FileTransmitter.stop(); } catch (Exception ex) { Timber.e(ex, "Error stopping file transmitter after failure (camera2)"); } h264FileTransmitter = null; } return 1; // 失败 } } /** * 忢æä»¶ä¼ è¾å¨ */ private int stopFileTransmitter() { Timber.d("Stopping file transmitter (camera2)"); if (h264FileTransmitter != null) { try { h264FileTransmitter.stop(); h264FileTransmitter = null; Timber.d("File transmitter stopped successfully (camera2)"); return 0; // æå } catch (Exception e) { Timber.e(e, "Error stopping file transmitter (camera2)"); h264FileTransmitter = null; return 1; // 失败 } } else { Timber.w("File transmitter is not running (camera2)"); return 0; // æåï¼æ²¡æè¿è¡çæä»¶ä¼ è¾å¨ï¼è§ä¸ºæåï¼ } } /** * è·åèµæºåè¡¨ï¼æ ¹æ®JT/T 1076-2016表23å®ä¹ï¼ */ private List<ResourceInfo> getResourceList(String startTime, String endTime) { Timber.d("getResourceList called (camera2), startTime: %s, endTime: %s", startTime, endTime); List<ResourceInfo> resourceList = new ArrayList<>(); try { // æ«æè¾åºç®å½ä¸çH264æä»¶ï¼åªæ¥æ¾camera2çæä»¶ï¼ File dir = new File(outputFileDirectory); if (!dir.exists() || !dir.isDirectory()) { Timber.w("Output directory does not exist: %s", outputFileDirectory); return resourceList; } File[] files = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".h264") && name.contains("camera2")); if (files == null || files.length == 0) { Timber.d("No H264 files found for camera2 in directory"); return resourceList; } // è§£ææ¶é´èå´ Date startDate = parseTime(startTime); Date endDate = parseTime(endTime); if (startDate == null || endDate == null) { Timber.e("Invalid time format, startTime: %s, endTime: %s", startTime, endTime); return resourceList; } // éåæä»¶ï¼æ¥æ¾å¨æ¶é´èå´å çæä»¶ for (File file : files) { ResourceInfo resourceInfo = createResourceInfoFromFile(file, startDate, endDate); if (resourceInfo != null) { resourceList.add(resourceInfo); } } Timber.d("Found %d resources for camera2 in time range", resourceList.size()); return resourceList; } catch (Exception e) { Timber.e(e, "Error getting resource list (camera2)"); return resourceList; } } /** * 设置水å°ä¿¡æ¯ */ private void setWatermarkInfo(String watermarkInfoJson) { Timber.d("setWatermarkInfo called (camera2), watermarkInfoJson: %s", watermarkInfoJson); try { if (watermarkInfoJson == null || watermarkInfoJson.trim().isEmpty()) { Timber.w("Watermark info JSON is null or empty, clearing watermark (camera2)"); currentWatermarkInfo = null; // 妿ç¼ç 卿£å¨è¿è¡ï¼æ¸ 餿°´å° if (h264Encoder != null) { h264Encoder.setWatermarkInfo(null); } return; } // è§£æJSON JSONObject json = new JSONObject(watermarkInfoJson); WatermarkInfo watermarkInfo = new WatermarkInfo(); // è§£æåä¸ªåæ®µï¼ä½¿ç¨ optString/optDouble é¿å åæ®µä¸å卿¶æåºå¼å¸¸ï¼ watermarkInfo.setPlateNumber(json.optString("plateNumber", null)); watermarkInfo.setStudent(json.optString("student", null)); watermarkInfo.setCoach(json.optString("coach", null)); // ç»åº¦å纬度å¯è½æ¯æ°åæå符串 if (json.has("longitude")) { Object lonObj = json.get("longitude"); if (lonObj instanceof Number) { watermarkInfo.setLongitude(((Number) lonObj).doubleValue()); } else if (lonObj instanceof String) { try { watermarkInfo.setLongitude(Double.parseDouble((String) lonObj)); } catch (NumberFormatException e) { Timber.w("Invalid longitude format: %s", lonObj); } } } if (json.has("latitude")) { Object latObj = json.get("latitude"); if (latObj instanceof Number) { watermarkInfo.setLatitude(((Number) latObj).doubleValue()); } else if (latObj instanceof String) { try { watermarkInfo.setLatitude(Double.parseDouble((String) latObj)); } catch (NumberFormatException e) { Timber.w("Invalid latitude format: %s", latObj); } } } watermarkInfo.setDrivingSchool(json.optString("drivingSchool", null)); // 车éå¯è½æ¯æ°åæå符串 if (json.has("speed")) { Object speedObj = json.get("speed"); if (speedObj instanceof Number) { watermarkInfo.setSpeed(((Number) speedObj).doubleValue()); } else if (speedObj instanceof String) { try { watermarkInfo.setSpeed(Double.parseDouble((String) speedObj)); } catch (NumberFormatException e) { Timber.w("Invalid speed format: %s", speedObj); } } } // ä¿åæ°´å°ä¿¡æ¯ currentWatermarkInfo = watermarkInfo; Timber.i("Watermark info parsed successfully (camera2): %s", watermarkInfo); // 妿ç¼ç 卿£å¨è¿è¡ï¼ç«å³åºç¨æ°´å° if (h264Encoder != null) { h264Encoder.setWatermarkInfo(watermarkInfo); Timber.d("Watermark applied to encoder (camera2)"); } else { Timber.d("Encoder not running, watermark will be applied when encoder starts (camera2)"); } } catch (JSONException e) { Timber.e(e, "Failed to parse watermark info JSON (camera2): %s", watermarkInfoJson); currentWatermarkInfo = null; } catch (Exception e) { Timber.e(e, "Unexpected error setting watermark info (camera2)"); currentWatermarkInfo = null; } } /** * 仿件åå»ºèµæºä¿¡æ¯ï¼å¦ææä»¶å¨æ¶é´èå´å ï¼ */ private ResourceInfo createResourceInfoFromFile(File file, Date startDate, Date endDate) { try { // 仿件å䏿忶鴿³ï¼æ ¼å¼ï¼h264_camera2_1234567890123.h264ï¼ String fileName = file.getName(); Date startTimeFromFileName = null; if (fileName.startsWith("h264_camera2_") && fileName.endsWith(".h264")) { try { // æåæä»¶åä¸çæ¶é´æ³ String timestampStr = fileName.substring(14, fileName.length() - 5); // 廿 "h264_camera2_" å ".h264" long timestamp = Long.parseLong(timestampStr); startTimeFromFileName = new Date(timestamp); } catch (NumberFormatException e) { Timber.w("Failed to parse timestamp from filename: %s", fileName); } } // å¦ææ æ³ä»æä»¶åè§£ææ¶é´æ³ï¼åä½¿ç¨æä»¶ä¿®æ¹æ¶é´ä½ä¸ºå¼å§æ¶é´ if (startTimeFromFileName == null) { startTimeFromFileName = new Date(file.lastModified()); } // ç»ææ¶é´ä½¿ç¨æä»¶ä¿®æ¹æ¶é´ Date endTimeFromFile = new Date(file.lastModified()); // æ£æ¥æä»¶æ¶é´æ¯å¦å¨æå®èå´å if (startTimeFromFileName.after(endDate) || endTimeFromFile.before(startDate)) { return null; // ä¸å¨æ¶é´èå´å } // åå»ºèµæºä¿¡æ¯å¯¹è±¡ ResourceInfo resourceInfo = new ResourceInfo(); // é»è¾ééå·ï¼ç¬¬äºä¸ªæå头使ç¨channelId=2ï¼ resourceInfo.setLogicalChannelNumber((byte) 2); // å¼å§æ¶é´ï¼ä»æä»¶åä¸çæ¶é´æ³ SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA); resourceInfo.setStartTime(bcdFormat.format(startTimeFromFileName)); // ç»ææ¶é´ï¼ä½¿ç¨æä»¶ä¿®æ¹æ¶é´ resourceInfo.setEndTime(bcdFormat.format(endTimeFromFile)); // æ¥è¦æ å¿ï¼é»è®¤å¼ï¼å®é åºä»æä»¶å æ°æ®è·åï¼ resourceInfo.setAlarmFlag(0L); // é³è§é¢èµæºç±»åï¼2-è§é¢ resourceInfo.setResourceType((byte) 2); // ç æµç±»åï¼1-ä¸»ç æµ resourceInfo.setStreamType((byte) 1); // åå¨å¨ç±»åï¼1-主åå¨å¨ resourceInfo.setStorageType((byte) 1); // æä»¶å¤§å° resourceInfo.setFileSize(file.length()); return resourceInfo; } catch (Exception e) { Timber.e(e, "Error creating resource info from file: %s", file.getName()); return null; } } /** * è§£æBCDæ¶é´å符串 */ private Date parseTime(String timeStr) { if (timeStr == null || timeStr.length() != 12) { return null; } try { SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA); return format.parse(timeStr); } catch (ParseException e) { Timber.e(e, "Failed to parse time: %s", timeStr); return null; } } } app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -76,8 +76,12 @@ // æä»¶è¾åº private FileOutputStream fileOutputStream; private String outputFilePath; private String outputFileDirectory; // è¾åºæä»¶ç®å½ private boolean enableFileOutput = false; // æ¯å¦å¯ç¨æä»¶è¾åº private boolean spsPpsWritten = false; // æ è®°SPS/PPSæ¯å¦å·²åå ¥ private int cameraId = 1; // æå头IDï¼é»è®¤ä¸º1ï¼ç¬¬ä¸ä¸ªæåå¤´ï¼ private long currentFileStartTime = 0; // å½åæä»¶çå¼å§æ¶é´ï¼æ¯«ç§ï¼ private static final long FILE_DURATION_MS = 60 * 1000; // æä»¶æ¶é¿ï¼1åéï¼æ¯«ç§ï¼ // ç½ç»ä¼ è¾æ§å¶ private boolean enableNetworkTransmission = true; // æ¯å¦å¯ç¨TCP/UDPç½ç»ä¼ è¾ @@ -135,6 +139,22 @@ */ public void setOutputFile(String filePath) { this.outputFilePath = filePath; // æåç®å½è·¯å¾ if (filePath != null && !filePath.isEmpty()) { File file = new File(filePath); File parentDir = file.getParentFile(); if (parentDir != null) { this.outputFileDirectory = parentDir.getAbsolutePath(); } } } /** * 设置æå头IDï¼ç¨äºçææä»¶åï¼ * @param cameraId æå头IDï¼1表示第ä¸ä¸ªæå头ï¼2表示第äºä¸ªæå头 */ public void setCameraId(int cameraId) { this.cameraId = cameraId; } /** @@ -241,6 +261,11 @@ */ public boolean initialize(int[] cameraIdRange, String cameraName, int[] resolution, boolean ayCamera) { try { // ä»cameraIdRange䏿åcameraIdï¼ä½¿ç¨ç¬¬ä¸ä¸ªå¼ï¼ if (cameraIdRange != null && cameraIdRange.length > 0) { this.cameraId = cameraIdRange[0]; } // 1. setenv usbCamera.setenv(); @@ -277,7 +302,7 @@ // æ´æ°å®é å辨ç width = actualResolution[0]; height = actualResolution[1]; Timber.d("Camera initialized with resolution: " + width + "x" + height); Timber.d("Camera initialized with resolution: " + width + "x" + height + ", cameraId: " + cameraId); // 3. åå§åH264ç¼ç å¨ initEncoder(); @@ -293,7 +318,7 @@ } // 5. åå§åæä»¶è¾åºï¼ä» å建æä»¶ï¼SPS/PPSå¨ç¬¬ä¸æ¬¡è¾åºæ¶åå ¥ï¼ if (enableFileOutput && outputFilePath != null && !outputFilePath.isEmpty()) { if (enableFileOutput) { if (!initFileOutput()) { Timber.w("File output initialization failed, continuing without file output"); } @@ -333,21 +358,35 @@ */ private boolean initFileOutput() { try { File file = new File(outputFilePath); File parentDir = file.getParentFile(); if (parentDir != null && !parentDir.exists()) { boolean created = parentDir.mkdirs(); if (!created && !parentDir.exists()) { Timber.e("Failed to create parent directory: " + parentDir.getAbsolutePath()); // 妿outputFileDirectory为空ï¼å°è¯ä»outputFilePathæå if (outputFileDirectory == null || outputFileDirectory.isEmpty()) { if (outputFilePath != null && !outputFilePath.isEmpty()) { File file = new File(outputFilePath); File parentDir = file.getParentFile(); if (parentDir != null) { outputFileDirectory = parentDir.getAbsolutePath(); } } } // 妿ä»ç¶æ²¡æç®å½ï¼ä½¿ç¨é»è®¤è·¯å¾ if (outputFileDirectory == null || outputFileDirectory.isEmpty()) { Timber.e("Output file directory is not set"); return false; } // å建ç®å½ï¼å¦æä¸åå¨ï¼ File dir = new File(outputFileDirectory); if (!dir.exists()) { boolean created = dir.mkdirs(); if (!created && !dir.exists()) { Timber.e("Failed to create output directory: " + outputFileDirectory); return false; } } fileOutputStream = new FileOutputStream(file); spsPpsWritten = false; Timber.d("File output initialized: " + outputFilePath); return true; // å建第ä¸ä¸ªæä»¶ return createNewFile(); } catch (Exception e) { Timber.e(e,"Initialize file output failed"); if (fileOutputStream != null) { @@ -361,9 +400,67 @@ return false; } } /** * åå»ºæ°æä»¶ï¼æ¯åéè°ç¨ä¸æ¬¡ï¼ * @return æ¯å¦æå */ private boolean createNewFile() { try { // å ³éæ§æä»¶ if (fileOutputStream != null) { try { fileOutputStream.flush(); fileOutputStream.close(); Timber.d("Closed previous file: " + outputFilePath); } catch (IOException e) { Timber.e(e, "Error closing previous file"); } fileOutputStream = null; } // çææ°æä»¶å long timeFile = System.currentTimeMillis() / 1000 * 1000; currentFileStartTime = timeFile; String fileName; if (cameraId == 2) { fileName = "h264_camera2_" + timeFile + ".h264"; } else { fileName = "h264_" + timeFile + ".h264"; } File newFile = new File(outputFileDirectory, fileName); outputFilePath = newFile.getAbsolutePath(); // åå»ºæ°æä»¶ fileOutputStream = new FileOutputStream(newFile); spsPpsWritten = false; // éç½®SPS/PPSæ è®°ï¼æ°æä»¶éè¦éæ°åå ¥ // å¦æå·²ç»æç¼åçSPS/PPSï¼ç«å³åå ¥æ°æä»¶ if (spsBuffer != null && ppsBuffer != null) { writeSpsPpsToFile(); Timber.d("SPS/PPS written to new file immediately"); } Timber.d("Created new file: " + outputFilePath); return true; } catch (Exception e) { Timber.e(e, "Failed to create new file"); if (fileOutputStream != null) { try { fileOutputStream.close(); } catch (IOException ie) { Timber.e(ie, "Close file output stream failed"); } fileOutputStream = null; } return false; } } /** * åå ¥SPS/PPSå°æä»¶ï¼ä»CSDæå ³é®å¸§æ°æ®ä¸æåï¼ * åå ¥SPS/PPSå°æä»¶ï¼ä¼å 使ç¨ç¼åçSPS/PPSï¼å¦åä»CSDè·åï¼ */ private void writeSpsPpsToFile() { if (!enableFileOutput || fileOutputStream == null || spsPpsWritten) { @@ -371,17 +468,29 @@ } try { // å°è¯ä»ç¼ç å¨è¾åºæ ¼å¼ä¸è·åCSD MediaFormat format = encoder.getOutputFormat(); ByteBuffer spsBuffer = format.getByteBuffer("csd-0"); // SPS ByteBuffer ppsBuffer = format.getByteBuffer("csd-1"); // PPS // ä¼å 使ç¨ç¼åçSPS/PPSï¼è¿äºæ¯Annex-Bæ ¼å¼ï¼å·²ç»å å«èµ·å§ç ï¼ if (spsBuffer != null && ppsBuffer != null) { // ç´æ¥åå ¥ç¼åçSPS/PPSï¼å·²ç»æ¯Annex-Bæ ¼å¼ï¼ fileOutputStream.write(spsBuffer); fileOutputStream.write(ppsBuffer); fileOutputStream.flush(); spsPpsWritten = true; Timber.d("SPS/PPS written to file from cache, SPS size: " + spsBuffer.length + ", PPS size: " + ppsBuffer.length); return; } // 妿ç¼å䏿²¡æï¼å°è¯ä»ç¼ç å¨è¾åºæ ¼å¼ä¸è·åCSD MediaFormat format = encoder.getOutputFormat(); ByteBuffer csdSpsBuffer = format.getByteBuffer("csd-0"); // SPS ByteBuffer csdPpsBuffer = format.getByteBuffer("csd-1"); // PPS if (csdSpsBuffer != null && csdPpsBuffer != null) { // CSDæ ¼å¼é常æ¯AVCCæ ¼å¼ï¼éè¦è½¬æ¢ä¸ºAnnex-B byte[] sps = new byte[spsBuffer.remaining()]; byte[] pps = new byte[ppsBuffer.remaining()]; spsBuffer.get(sps); ppsBuffer.get(pps); byte[] sps = new byte[csdSpsBuffer.remaining()]; byte[] pps = new byte[csdPpsBuffer.remaining()]; csdSpsBuffer.get(sps); csdPpsBuffer.get(pps); // åå ¥SPSåPPSå°æä»¶ï¼Annex-Bæ ¼å¼ï¼ byte[] nalStartCode = {0x00, 0x00, 0x00, 0x01}; @@ -415,9 +524,9 @@ fileOutputStream.flush(); spsPpsWritten = true; Timber.d("SPS/PPS written to file, SPS size: " + spsLength + ", PPS size: " + ppsLength); Timber.d("SPS/PPS written to file from CSD, SPS size: " + spsLength + ", PPS size: " + ppsLength); } else { Timber.w("SPS/PPS not found in CSD, will extract from first key frame"); Timber.w("SPS/PPS not found in cache or CSD, will extract from first key frame"); } } catch (Exception e) { Timber.e(e,"Write SPS/PPS to file error"); @@ -680,6 +789,17 @@ } try { // æ£æ¥æ¯å¦éè¦åå»ºæ°æä»¶ï¼æ¯åéï¼ long currentTime = System.currentTimeMillis(); if (currentFileStartTime > 0 && (currentTime - currentFileStartTime) >= FILE_DURATION_MS) { Timber.d("File duration reached 1 minute, creating new file"); if (!createNewFile()) { Timber.e("Failed to create new file, stopping file output"); enableFileOutput = false; return; } } // 妿æ¯ç¬¬ä¸ä¸ªå ³é®å¸§ï¼ç¡®ä¿SPS/PPSå·²åå ¥ if (isKeyFrame && !spsPpsWritten) { writeSpsPpsToFile(); app/src/main/java/com/anyun/h264/MainActivity.kt
@@ -1,6 +1,8 @@ package com.anyun.h264 import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -11,6 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.anyun.h264.H264FileTransmitter.OnTransmitProgressCallback import com.anyun.h264.ui.theme.MyApplicationTheme import timber.log.Timber import java.io.File @@ -18,6 +21,9 @@ class MainActivity : ComponentActivity() { private var h264Encoder: H264Encoder? = null private var transmitter: H264FileTransmitter? = null private var fileList: List<File> = emptyList() private var currentFileIndex: Int = 0 private val handler = Handler(Looper.getMainLooper()) companion object{ const val TAG ="MainActivity" } @@ -34,13 +40,14 @@ modifier = Modifier.padding(innerPadding), isRunning = isRunning, onStartH264Click = { val success = startH264Encoder() val success = startFileTransmitter() if (success) { isRunning = true } }, onStopH264Click = { stopH264Encoder() // stopH264Encoder() stopFileTransmitter() isRunning = false } ) @@ -52,16 +59,37 @@ override fun onDestroy() { super.onDestroy() stopH264Encoder() stopFileTransmitter() } private fun startFileTransmitter():Boolean { if (transmitter != null) { Timber.w("H264Encoder is already running") Timber.w("FileTransmitter is already running") return false } // è·åç®å½ä¸çææ .h264 æä»¶ val directory = application.applicationContext.getExternalFilesDir(null) Timber.i("è§é¢ç®å½=${directory?.absolutePath}") if (directory?.isDirectory != true) { Timber.e("Directory is not valid: ${directory?.absolutePath}") return false } // è·åææ .h264 æä»¶å¹¶ææä»¶åæåº fileList = directory.listFiles() ?.filter { it.isFile && it.name.endsWith(".h264", ignoreCase = true) } ?.sortedBy { it.name } ?: emptyList() if (fileList.isEmpty()) { Timber.w("No .h264 files found in directory") return false } Timber.i("Found ${fileList.size} .h264 files to transmit") currentFileIndex = 0 try { transmitter = H264FileTransmitter() @@ -69,28 +97,99 @@ transmitter?.setServerAddress("192.168.16.138", 1078) transmitter?.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP) // æ PROTOCOL_TYPE_UDP // 设置åè®®åæ° transmitter?.setProtocolParams("013120122580", 1.toByte()) // 设置帧çï¼ç¨äºè®¡ç®æ¶é´æ³é´éï¼ transmitter?.setFrameRate(25) transmitter?.setOnTransmitProgressCallback(object : OnTransmitProgressCallback { override fun onProgress(currentFrame: Int, totalFrames: Int) { val currentFile = if (currentFileIndex < fileList.size) fileList[currentFileIndex] else null Timber.d("Transmitting file ${currentFileIndex + 1}/${fileList.size}: ${currentFile?.name}, frame: $currentFrame") } override fun onComplete() { val currentFile = if (currentFileIndex < fileList.size) fileList[currentFileIndex] else null Timber.i("File transmission complete: ${currentFile?.name} (${currentFileIndex + 1}/${fileList.size})") // ä½¿ç¨ Handler å»¶è¿è°ç¨ï¼ç¡®ä¿åä¸ä¸ªæä»¶çä¼ è¾ç¶æå·²éç½® currentFileIndex++ handler.postDelayed({ transmitNextFile() }, 100) // å»¶è¿100msï¼ç¡®ä¿åä¸ä¸ªæä»¶ç finally åå·²æ§è¡ } override fun onError(error: String?) { val currentFile = if (currentFileIndex < fileList.size) fileList[currentFileIndex] else null Timber.e("File transmission error: ${currentFile?.name}, error: $error") // å³ä½¿åºéä¹ç»§ç»ä¼ è¾ä¸ä¸ä¸ªæä»¶ // ä½¿ç¨ Handler å»¶è¿è°ç¨ï¼ç¡®ä¿åä¸ä¸ªæä»¶çä¼ è¾ç¶æå·²éç½® currentFileIndex++ handler.postDelayed({ transmitNextFile() }, 100) // å»¶è¿100msï¼ç¡®ä¿åä¸ä¸ªæä»¶ç finally åå·²æ§è¡ } }) // åå§åSocket if (transmitter?.initialize()==true) { // å¼å§ä¼ è¾æä»¶ transmitter?.transmitFile("/storage/emulated/0/Android/data/com.anyun.h264/files/h264_1764574451071.h264") if (transmitter?.initialize() == true) { // å¼å§ä¼ è¾ç¬¬ä¸ä¸ªæä»¶ transmitNextFile() return true }else{ } else { Timber.e("Failed to initialize transmitter") transmitter = null return false } } catch (e: Exception) { Timber.e(e, "Failed to start H264Encoder") Timber.e(e, "Failed to start FileTransmitter") transmitter = null return false } } /** * ä¼ è¾ä¸ä¸ä¸ªæä»¶ */ private fun transmitNextFile() { if (transmitter == null) { Timber.w("Transmitter is null, cannot transmit next file") return } if (currentFileIndex >= fileList.size) { Timber.i("All files transmission complete! Total: ${fileList.size} files") // æææä»¶ä¼ è¾å®æï¼åæ¢ä¼ è¾å¨ stopFileTransmitter() return } val nextFile = fileList[currentFileIndex] Timber.i("Starting transmission of file ${currentFileIndex + 1}/${fileList.size}: ${nextFile.name}") // ä¼ è¾ä¸ä¸ä¸ªæä»¶ transmitter?.transmitFile(nextFile.absolutePath) } /** * 忢æä»¶ä¼ è¾å¨ */ private fun stopFileTransmitter() { // ç§»é¤ææå¾ å¤çç Handler ä»»å¡ handler.removeCallbacksAndMessages(null) transmitter?.let { tx -> try { tx.stop() Timber.d("FileTransmitter stopped") } catch (e: Exception) { Timber.e(e, "Failed to stop FileTransmitter") } } transmitter = null fileList = emptyList() currentFileIndex = 0 } private fun startH264Encoder(): Boolean { @@ -109,11 +208,11 @@ // 设置è¾åºæä»¶ï¼å¯éï¼ val outputFile = File(getExternalFilesDir(null), "test2.h264") h264Encoder?.setOutputFile(outputFile.absolutePath) h264Encoder?.setEnableFileOutput(false) // å¯ç¨æä»¶è¾åº h264Encoder?.setEnableFileOutput(true) // å¯ç¨æä»¶è¾åº // 设置UDPæå¡å¨å°åï¼å¯éï¼ // h264Encoder?.setServerAddress("58.48.93.67", 11935) h264Encoder?.setEnableNetworkTransmission(true) h264Encoder?.setEnableNetworkTransmission(false) h264Encoder?.setServerAddress("192.168.16.138", 1078) h264Encoder?.setProtocolParams("013120122580", 1) ¶à½ø³Ì·½°¸Ê¹ÓÃ˵Ã÷.md
New file @@ -0,0 +1,117 @@ # å¤è¿ç¨æ¹æ¡ä½¿ç¨è¯´æ ## æ¦è¿° æ¬æ¹æ¡éè¿å¤è¿ç¨å®ç°ä¸¤ä¸ªUSBæå头忶工ä½ãæ¯ä¸ªæå头è¿è¡å¨ç¬ç«çè¿ç¨ä¸ï¼æ¯ä¸ªè¿ç¨æ¥æç¬ç«çH264Encoderå®ä¾ï¼ä»èé¿å äºåºå±Cåºçåå®ä¾éå¶ã ## æ¶æè¯´æ - **H264EncodeService**ï¼ä¸»è¿ç¨æå¡ï¼å¤ç第ä¸ä¸ªæå头ï¼cameraId=1ï¼ - **H264EncodeService2**ï¼ç¬ç«è¿ç¨æå¡ï¼`:camera2`ï¼ï¼å¤ç第äºä¸ªæå头ï¼cameraId=2ï¼ ## ä½¿ç¨æ¹æ³ ### å¯å¨ç¬¬ä¸ä¸ªæå头ï¼cameraId=1ï¼ ```json { "width": 640, "height": 480, "framerate": 25, "cameraId": 1 } ``` æè 䏿å®cameraIdï¼é»è®¤ä¸º1ï¼ï¼ ```json { "width": 640, "height": 480, "framerate": 25 } ``` ### å¯å¨ç¬¬äºä¸ªæå头ï¼cameraId=2ï¼ ```json { "width": 640, "height": 480, "framerate": 25, "cameraId": 2 } ``` ### ç½ç»æ¨éç¤ºä¾ **第ä¸ä¸ªæå头ï¼** ```json { "ip": "192.168.1.100", "port": 8888, "width": 640, "height": 480, "framerate": 25, "simPhone": "013120122580", "cameraId": 1 } ``` **第äºä¸ªæå头ï¼** ```json { "ip": "192.168.1.100", "port": 8889, "width": 640, "height": 480, "framerate": 25, "simPhone": "013120122580", "cameraId": 2 } ``` ### 忢æå®æå头 åæ¢ç¬¬ä¸ä¸ªæåå¤´ï¼ ```json { "cameraId": 1 } ``` åæ¢ç¬¬äºä¸ªæåå¤´ï¼ ```json { "cameraId": 2 } ``` 妿䏿å®cameraIdï¼é»è®¤åæ¢ç¬¬ä¸ä¸ªæå头ã ## å ³é®ç¹æ§ 1. **è¿ç¨é离**ï¼ä¸¤ä¸ªæå头è¿è¡å¨å®å ¨ç¬ç«çè¿ç¨ä¸ï¼äºä¸å¹²æ° 2. **èªå¨è·¯ç±**ï¼ä¸»æå¡æ ¹æ®cameraIdèªå¨è·¯ç±å°å¯¹åºçè¿ç¨ 3. **ç¬ç«é ç½®**ï¼æ¯ä¸ªæå头å¯ä»¥ç¬ç«é ç½®å辨çã帧çãç½ç»åæ°ç 4. **ç¬ç«æä»¶**ï¼ç¬¬äºä¸ªæå头çæä»¶ä¼æ·»å `camera2`æ è¯ï¼ä¾¿äºåºå 5. **ç¬ç«éé**ï¼ç¬¬äºä¸ªæå头使ç¨`logicalChannelId=2`ï¼ç¬¬ä¸ä¸ªä½¿ç¨`logicalChannelId=1` ## æä»¶å½å - 第ä¸ä¸ªæå头ï¼`h264_1234567890123.h264` - 第äºä¸ªæå头ï¼`h264_camera2_1234567890123.h264` ## 注æäºé¡¹ 1. ç¡®ä¿ä¸¤ä¸ªUSBæå头é½å·²æ£ç¡®è¿æ¥ 2. 第äºä¸ªæå头åºå®ä½¿ç¨cameraId=2ï¼ç¬¬ä¸ä¸ªæå头使ç¨cameraId=1ï¼æä¸æå®ï¼ 3. 两个è¿ç¨å¯ä»¥åæ¶è¿è¡ï¼äºä¸å½±å 4. æ¯ä¸ªè¿ç¨é½æç¬ç«çH264Encoderå®ä¾ï¼å æ¤å¯ä»¥çæ£å¹¶åå·¥ä½ ## ææ¯å®ç° - 主æå¡éè¿AIDLç»å®å°ç¬¬äºä¸ªè¿ç¨çæå¡ - 第äºä¸ªè¿ç¨éè¿`android:process=":camera2"`é ç½®å¨ç¬ç«è¿ç¨ä¸è¿è¡ - æææä½é½éè¿AIDLæ¥å£è¿è¡è¿ç¨é´éä¿¡