README_H264_CHECK.md
@@ -118,3 +118,5 @@ ``` åºè¯¥çå°ï¼`00 00 00 01` æ `00 00 01`ï¼Annex-Bèµ·å§ç ï¼ app/build.gradle
@@ -15,7 +15,7 @@ minSdk 21 targetSdk 35 versionCode 1 versionName "1.0" versionName "1.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } app/src/main/aidl/com/anyun/h264/IH264EncodeService.aidl
@@ -32,12 +32,12 @@ * @param endTime ç»ææ¶é´ï¼æ ¼å¼ï¼YYMMDDHHmmssï¼BCDç¼ç ç6åèåç¬¦ä¸²ï¼ * @return èµæºåè¡¨ï¼æ ¹æ®JT/T 1076-2016表23å®ä¹ï¼ */ List<ResourceInfo> getResourceList(String startTime, String endTime); List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard, String jsonConfig); /** * 设置水å°ä¿¡æ¯ * @param watermarkInfo æ°´å°ä¿¡æ¯å符串 */ void setWatermarkInfo(String watermarkInfo); void setWatermarkInfo(String watermarkInfo, String jsonConfig); } app/src/main/aidl/com/anyun/h264/model/ResourceInfo.aidl
@@ -5,3 +5,5 @@ */ parcelable ResourceInfo; app/src/main/java/com/anyun/h264/H264EncodeService.java
@@ -11,6 +11,7 @@ import com.anyun.h264.model.ResourceInfo; import com.anyun.h264.model.WatermarkInfo; import com.anyun.h264.util.FileUtil; import org.json.JSONException; import org.json.JSONObject; @@ -19,6 +20,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; @@ -36,7 +38,7 @@ private H264FileTransmitter h264FileTransmitter; // H264æä»¶ä¼ è¾å¨ private String outputFileDirectory; // H264æä»¶è¾åºç®å½ private WatermarkInfo currentWatermarkInfo; // å½åæ°´å°ä¿¡æ¯ private static final int H264_FILE_RETENTION_DAYS = 5; // 坿 ¹æ®éæ±è°æ´ä¸º3æ5天 private static final int H264_FILE_RETENTION_DAYS = 1; // 坿 ¹æ®éæ±è°æ´ä¸º3æ5天 // å¤è¿ç¨æ¯æï¼ç¬¬äºä¸ªæå头çæå¡è¿æ¥ private IH264EncodeService camera2Service; @@ -61,13 +63,13 @@ } @Override public List<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException { return H264EncodeService.this.getResourceList(startTime, endTime); public List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) throws RemoteException { return H264EncodeService.this.getResourceList(startTime, endTime,useTFCard, jsonConfig); } @Override public void setWatermarkInfo(String watermarkInfo) throws RemoteException { H264EncodeService.this.setWatermarkInfo(watermarkInfo); public void setWatermarkInfo(String watermarkInfo,String jsonConfig) throws RemoteException { H264EncodeService.this.setWatermarkInfo(watermarkInfo,jsonConfig); } }; @@ -123,6 +125,9 @@ * ç¼ç é 置类 */ private static class EncodeConfig { boolean useTFCard=true;//å¦æä¸ºtrueï¼åå¨tfcard æ ¹ç®å½å»ºä¸ä¸ªh264æä»¶å¤¹ï¼å¨h264ç®å½ä¸ä»¥å½åæ¥æä¸ºååçæä»¶å¤¹ï¼æ¯å¦20250123ï¼ï¼ç¶åh264æä»¶å°±åå ¥ä»¥å½åæ¥å为ååçç®å½ä¸ boolean enableFileOutput; //æ¯å¦å¼å¯h264æä»¶åå ¥ boolean enableNetworkTransmit; //å¼å¯h264ï¼ç½ç»å®æ¶æ¨æµ String ip; int port; int width; @@ -139,10 +144,13 @@ config.width = DEFAULT_WIDTH; config.height = DEFAULT_HEIGHT; config.framerate = DEFAULT_FRAME_RATE; config.enableFileOutput = false; config.enableNetworkTransmit = false; config.ip = null; config.port = 0; config.simPhone = null; config.cameraId = 1; // é»è®¤ä½¿ç¨ç¬¬ä¸ä¸ªæå头 config.useTFCard = false; // é»è®¤ä¸ä½¿ç¨TFå¡ return config; } @@ -150,9 +158,12 @@ config.width = json.optInt("width", DEFAULT_WIDTH); config.height = json.optInt("height", DEFAULT_HEIGHT); config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE); config.enableFileOutput = json.optBoolean("enableFileOutput", false); config.enableNetworkTransmit = json.optBoolean("enableNetworkTransmit", false); config.ip = json.optString("ip", null); config.port = json.optInt("port", 0); config.simPhone = json.optString("simPhone", null); config.useTFCard = json.optBoolean("useTFCard", false); // è§£æcameraIdï¼å¦ææªæå®ï¼é»è®¤ä¸º1ï¼ if (json.has("cameraId")) { @@ -238,7 +249,9 @@ case 0: // å¼å¯h264æä»¶åå ¥ try { EncodeConfig config0 = EncodeConfig.fromJson(jsonConfig); return startFileEncode(config0); config0.enableFileOutput = true; config0.enableNetworkTransmit = false; return startEncode(config0); } catch (JSONException e) { Timber.e(e, "Failed to parse JSON config: %s", jsonConfig); return 1; @@ -251,21 +264,35 @@ } return stopEncoder(); case 2: // å¼å¯ç½ç»æ¨éh264ï¼ä¸åå ¥æä»¶ï¼ case 2: // å¼å¯ç½ç»æ¨éh264ï¼å¯åæ¶åå ¥æä»¶ï¼ try { EncodeConfig config2 = EncodeConfig.fromJson(jsonConfig); return startNetworkEncode(config2); // æ£æ¥å¿ éçé ç½®åæ° if (config2 == null || config2.ip == null || config2.ip.trim().isEmpty() || config2.port <= 0) { Timber.e("Network encode requires valid ip and port in config"); return 1; // 失败 } config2.enableNetworkTransmit = true; return startEncode(config2); } catch (JSONException e) { Timber.e(e, "Failed to parse JSON config: %s", jsonConfig); return 1; } case 3: // 忢h264ç¼ç 并忢ç½ç»æ¨é case 3: // 忢ç½ç»æ¨éï¼ä¿ææä»¶åå ¥ï¼ // æ£æ¥æ¯å¦æå®äºcameraId=2 if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } return stopEncoder(); // åªå ³éç½ç»ä¼ è¾ï¼ä¿ææä»¶åå ¥ if (h264Encoder != null) { h264Encoder.setEnableNetworkTransmission(false); Timber.d("Network transmission stopped, file output continues"); return 0; } else { Timber.w("Encoder is not running"); return 0; // æåï¼æ²¡æè¿è¡çç¼ç å¨ï¼è§ä¸ºæåï¼ } case 4: // å¼å§ä¼ è¾H264æä»¶ try { @@ -317,6 +344,56 @@ } catch (RemoteException e) { Timber.e(e, "Error calling camera2 service"); return 1; } } /** * å¨ç¬¬äºä¸ªè¿ç¨ï¼camera2ï¼ä¸è·åèµæºå表 */ private List<ResourceInfo> getResourceListInProcess2(String startTime, String endTime, boolean useTFCard, String jsonConfig) { Timber.d("Routing to process 2 (camera2) for getResourceList"); try { // ç¡®ä¿ç¬¬äºä¸ªè¿ç¨çæå¡å·²ç»å® if (!ensureCamera2ServiceBound()) { Timber.e("Failed to bind camera2 service"); return new ArrayList<>(); } // è°ç¨ç¬¬äºä¸ªè¿ç¨çæå¡ if (camera2Service != null) { return camera2Service.getResourceList(startTime, endTime, useTFCard, jsonConfig); } else { Timber.e("Camera2 service is null"); return new ArrayList<>(); } } catch (RemoteException e) { Timber.e(e, "Error calling camera2 service for getResourceList"); return new ArrayList<>(); } } /** * å¨ç¬¬äºä¸ªè¿ç¨ï¼camera2ï¼ä¸è®¾ç½®æ°´å°ä¿¡æ¯ */ private void setWatermarkInfoInProcess2(String watermarkInfoJson, String jsonConfig) { Timber.d("Routing to process 2 (camera2) for setWatermarkInfo"); try { // ç¡®ä¿ç¬¬äºä¸ªè¿ç¨çæå¡å·²ç»å® if (!ensureCamera2ServiceBound()) { Timber.e("Failed to bind camera2 service"); return; } // è°ç¨ç¬¬äºä¸ªè¿ç¨çæå¡ if (camera2Service != null) { camera2Service.setWatermarkInfo(watermarkInfoJson, jsonConfig); } else { Timber.e("Camera2 service is null"); } } catch (RemoteException e) { Timber.e(e, "Error calling camera2 service for setWatermarkInfo"); } } @@ -374,87 +451,113 @@ } /** * å¯å¨æä»¶ç¼ç 模å¼ï¼åªåå ¥æä»¶ï¼ä¸è¿è¡ç½ç»æ¨éï¼ * è·åè¾åºæä»¶ç®å½ï¼æ ¹æ®useTFCardé ç½®ï¼ * @param useTFCard æ¯å¦ä½¿ç¨TFå¡ * @return è¾åºç®å½è·¯å¾ */ private int startFileEncode(EncodeConfig config) { Timber.d("Starting file encode mode"); // 妿ç¼ç å¨å·²ç»å¨è¿è¡ï¼å 忢 if (h264Encoder != null) { Timber.w("Encoder is already running, 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éæ©æå头èå´ 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(cameraIdRange, null, resolution, false)) { // åºç¨å·²ä¿åçæ°´å°ä¿¡æ¯ï¼å¦ææï¼ if (currentWatermarkInfo != null) { h264Encoder.setWatermarkInfo(currentWatermarkInfo); Timber.d("Applied saved watermark info to encoder"); private String getOutputFileDirectory(boolean useTFCard) { if (useTFCard) { // 使ç¨TFå¡ï¼/sdcard/h264/å½åæ¥æ/ try { String storagePath = FileUtil.getStoragePath(this, true); if (storagePath == null || storagePath.trim().isEmpty()) { Timber.w("TF card storage path not available, fallback to app directory"); return outputFileDirectory; } h264Encoder.start(); 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"); h264Encoder = null; return 1; // 失败 File externalStorage = new File(storagePath); if (!externalStorage.exists()) { Timber.w("TF card storage directory does not exist: %s, fallback to app directory", storagePath); return outputFileDirectory; } // è·åå½åæ¥æï¼æ ¼å¼ï¼yyyyMMddï¼å¦20250123ï¼ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); String dateDir = dateFormat.format(new Date()); // æå»ºè·¯å¾ï¼/sdcard/h264/20250123/ File h264Dir = new File(externalStorage, "h264"); File dateDirFile = new File(h264Dir, dateDir); // å建ç®å½ï¼å¦æä¸åå¨ï¼ if (!dateDirFile.exists()) { boolean created = dateDirFile.mkdirs(); if (!created && !dateDirFile.exists()) { Timber.e("Failed to create TF card directory: %s, fallback to app directory", dateDirFile.getAbsolutePath()); return outputFileDirectory; } } String tfCardPath = dateDirFile.getAbsolutePath(); Timber.d("Using TF card directory: %s", tfCardPath); return tfCardPath; } catch (Exception e) { Timber.e(e, "Error getting TF card directory, fallback to app directory"); return outputFileDirectory; } } catch (Exception e) { Timber.e(e, "Failed to start file encode"); h264Encoder = null; return 1; // 失败 } else { // 使ç¨åºç¨å¤é¨åå¨ç®å½ return outputFileDirectory; } } /** * å¯å¨ç½ç»æ¨é模å¼ï¼åªè¿è¡ç½ç»æ¨éï¼ä¸åå ¥æä»¶ï¼ * å¯å¨ç¼ç ï¼ç»ä¸æ¹æ³ï¼æ¯ææä»¶åå ¥åç½ç»ä¼ è¾çç»åï¼ * @param config ç¼ç é ç½® * @return 0-æåï¼1-失败 */ private int startNetworkEncode(EncodeConfig config) { Timber.d("Starting network encode mode"); private int startEncode(EncodeConfig config) { if (config == null) { Timber.e("Encode config cannot be null"); return 1; } Timber.d("Starting encode mode, fileOutput: %b, networkTransmit: %b, useTFCard: %b", config.enableFileOutput, config.enableNetworkTransmit, config.useTFCard); // 妿ç¼ç å¨å·²ç»å¨è¿è¡ï¼å 忢 // 妿ç¼ç å¨å·²ç»å¨è¿è¡ï¼åªæ´æ°é ç½® if (h264Encoder != null) { Timber.w("Encoder is already running, stopping it first"); stopEncoder(); Timber.d("Encoder is already running, updating configuration"); try { h264Encoder.setEnableFileOutput(config.enableFileOutput); // 妿å¼å¯ç½ç»ä¼ è¾ï¼éè¦è®¾ç½®æå¡å¨å°åååè®®åæ° if (config.enableNetworkTransmit) { if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) { Timber.e("Network transmit requires valid ip and port in config"); return 1; // 失败 } h264Encoder.setServerAddress(config.ip, config.port); // 设置åè®®åæ°ï¼ä½¿ç¨é ç½®ä¸çsimPhoneï¼å¦ææªæä¾å使ç¨é»è®¤å¼ï¼ String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty() ? config.simPhone : "013120122580"; h264Encoder.setProtocolParams(simPhone, (byte)1); } h264Encoder.setEnableNetworkTransmission(config.enableNetworkTransmit); Timber.d("Encoder configuration updated successfully"); return 0; // æå } catch (Exception e) { Timber.e(e, "Error updating encoder configuration"); return 1; // 失败 } } // æ£æ¥å¿ éçé ç½®åæ° if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) { Timber.e("Network encode requires valid ip and port in config"); return 1; // 失败 } // ç¼ç 卿ªè¿è¡ï¼éè¦åå§åå¹¶å¯å¨ try { // 妿å¼å¯ç½ç»ä¼ è¾ï¼æ£æ¥å¿ éçé ç½®åæ° if (config.enableNetworkTransmit) { if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) { Timber.e("Network transmit requires valid ip and port in config"); return 1; // 失败 } } // å建ç¼ç å¨ h264Encoder = new H264Encoder(); // 设置 Contextï¼ç¨äºæ¸ ç TF å¡æä»¶ï¼ h264Encoder.setContext(this); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH; @@ -462,29 +565,32 @@ 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); // è·åè¾åºæä»¶ç®å½ï¼æ ¹æ®useTFCardé ç½®ï¼ String outputDir = getOutputFileDirectory(config.useTFCard); // 设置åè®®åæ°ï¼ä½¿ç¨é ç½®ä¸çsimPhoneï¼å¦ææªæä¾å使ç¨é»è®¤å¼ï¼ String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty() ? config.simPhone : "013120122580"; h264Encoder.setProtocolParams(simPhone, (byte)1); // 设置è¾åºæä»¶ç®å½ï¼H264Encoderä¼èªå¨ç®¡çæä»¶åå»ºï¼æ¯åéä¸ä¸ªæä»¶ï¼ // 使ç¨ä¸ä¸ªä¸´æ¶æä»¶åæ¥è®¾ç½®ç®å½ï¼H264Encoderä¼å¨åå§åæ¶å建第ä¸ä¸ªæä»¶ File tempFile = new File(outputDir, "temp.h264"); h264Encoder.setOutputFile(tempFile.getAbsolutePath()); h264Encoder.setEnableFileOutput(config.enableFileOutput); // 设置ç½ç»ä¼ è¾ h264Encoder.setEnableNetworkTransmission(config.enableNetworkTransmit); if (config.enableNetworkTransmit) { h264Encoder.setServerAddress(config.ip, config.port); // 设置åè®®åæ°ï¼ä½¿ç¨é ç½®ä¸çsimPhoneï¼å¦ææªæä¾å使ç¨é»è®¤å¼ï¼ String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty() ? config.simPhone : "013120122580"; 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(cameraIdRange, null, resolution, false)) { // åºç¨å·²ä¿åçæ°´å°ä¿¡æ¯ï¼å¦ææï¼ @@ -493,8 +599,11 @@ Timber.d("Applied saved watermark info to encoder"); } h264Encoder.start(); Timber.d("Network encode started successfully, server: %s:%d, resolution: %dx%d, framerate: %d", config.ip, config.port, width, height, framerate); Timber.d("Encode started successfully, fileOutput: %b, networkTransmit: %b, resolution: %dx%d, framerate: %d", config.enableFileOutput, config.enableNetworkTransmit, width, height, framerate); if (config.enableNetworkTransmit) { Timber.d("Network server: %s:%d", config.ip, config.port); } return 0; // æå } else { Timber.e("Failed to initialize encoder"); @@ -502,7 +611,7 @@ return 1; // 失败 } } catch (Exception e) { Timber.e(e, "Failed to start network encode"); Timber.e(e, "Failed to start encode"); h264Encoder = null; return 1; // 失败 } @@ -556,11 +665,48 @@ } try { // æ£æ¥æä»¶æ¯å¦åå¨ File file = new File(config.filePath); // è§£æå¾ ä¼ è¾æä»¶è·¯å¾ï¼è¥ä¸åå¨åå°è¯å°TFå¡ç®å½ææ¥ææ¥æ¾ String resolvedFilePath = config.filePath; File file = new File(resolvedFilePath); if (!file.exists() || !file.isFile()) { Timber.e("File does not exist: %s", config.filePath); return 1; // 失败 Timber.w("File does not exist, try TF card lookup: %s", resolvedFilePath); String fileName = file.getName(); String timestampStr = null; if (fileName.startsWith("h264_") && fileName.endsWith(".h264")) { timestampStr = fileName.substring(5, fileName.length() - 5); } if (timestampStr == null || timestampStr.trim().isEmpty()) { Timber.e("Cannot parse timestamp from file name: %s", fileName); return 1; // 失败 } try { long timestamp = Long.parseLong(timestampStr); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); String dateDir = dateFormat.format(new Date(timestamp)); String storagePath = FileUtil.getStoragePath(this, true); if (storagePath == null || storagePath.trim().isEmpty()) { Timber.e("TF card storage path not available when searching file"); return 1; // 失败 } File tfRoot = new File(storagePath, "h264"); File candidate = new File(new File(tfRoot, dateDir), fileName); if (candidate.exists() && candidate.isFile()) { resolvedFilePath = candidate.getAbsolutePath(); file = candidate; Timber.i("Found file on TF card: %s", resolvedFilePath); } else { Timber.e("File not found on TF card path: %s", candidate.getAbsolutePath()); return 1; // 失败 } } catch (NumberFormatException e) { Timber.e(e, "Failed to parse timestamp from file name: %s", fileName); return 1; // 失败 } } // å建æä»¶ä¼ è¾å¨ @@ -585,8 +731,8 @@ h264FileTransmitter.setOnTransmitProgressCallback(new H264FileTransmitter.OnTransmitProgressCallback() { @Override public void onProgress(int currentFrame, int totalFrames) { Timber.d("File transmit progress: frame %d%s", currentFrame, totalFrames > 0 ? " of " + totalFrames : ""); // Timber.d("File transmit progress: frame %d%s", currentFrame, // totalFrames > 0 ? " of " + totalFrames : ""); } @Override @@ -609,10 +755,10 @@ } // å¼å§ä¼ è¾æä»¶ h264FileTransmitter.transmitFile(config.filePath); h264FileTransmitter.transmitFile(resolvedFilePath); Timber.d("File transmit started successfully, file: %s, server: %s:%d, protocol: %s, framerate: %d", config.filePath, config.ip, config.port, resolvedFilePath, config.ip, config.port, config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP", framerate); return 0; // æå @@ -659,31 +805,110 @@ * @param endTime ç»ææ¶é´ï¼æ ¼å¼ï¼YYMMDDHHmmssï¼ * @return èµæºå表 */ private List<ResourceInfo> getResourceList(String startTime, String endTime) { Timber.d("getResourceList called, startTime: %s, endTime: %s", startTime, endTime); private List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) { Timber.d("getResourceList called, startTime: %s, endTime: %s, useTFCard: %b", startTime, endTime, useTFCard); // è§£æ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) { // 忽ç¥è§£æé误ï¼ç»§ç»ä½¿ç¨å½åè¿ç¨ Timber.w(e, "Failed to parse cameraId from jsonConfig"); } } // 妿æå®äºcameraId=2ï¼è·¯ç±å°ç¬¬äºä¸ªè¿ç¨ if (cameraId != null && cameraId == 2) { return getResourceListInProcess2(startTime, endTime, useTFCard, jsonConfig); } List<ResourceInfo> resourceList = new ArrayList<>(); try { // æ«æè¾åºç®å½ä¸çH264æä»¶ 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")); if (files == null || files.length == 0) { Timber.d("No H264 files found 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; } if (useTFCard) { // 使ç¨TFå¡ï¼æ«æTFå¡ä¸çh264æä»¶å¤¹ï¼æ ¹æ®æ¥æèå´è¿æ»¤ String storagePath = FileUtil.getStoragePath(this, true); if (storagePath == null || storagePath.trim().isEmpty()) { Timber.w("TF card storage path not available, fallback to app directory"); // åéå°åºç¨ç®å½ return getResourceListFromDirectory(outputFileDirectory, startDate, endDate); } File externalStorage = new File(storagePath); if (!externalStorage.exists()) { Timber.w("TF card storage directory does not exist: %s, fallback to app directory", storagePath); // åéå°åºç¨ç®å½ return getResourceListFromDirectory(outputFileDirectory, startDate, endDate); } // TFå¡ä¸çh264æä»¶å¤¹è·¯å¾ï¼/sdcard/h264/ File h264Dir = new File(externalStorage, "h264"); if (!h264Dir.exists() || !h264Dir.isDirectory()) { Timber.w("TF card h264 directory does not exist: %s", h264Dir.getAbsolutePath()); return resourceList; } // è·åæ¥æèå´å çæææ¥ææä»¶å¤¹ List<String> dateDirs = getDateDirectoriesInRange(startDate, endDate); Timber.d("Found %d date directories in range", dateDirs.size()); // æ«ææ¯ä¸ªæ¥ææä»¶å¤¹ä¸çh264æä»¶ for (String dateDir : dateDirs) { File dateDirFile = new File(h264Dir, dateDir); if (dateDirFile.exists() && dateDirFile.isDirectory()) { List<ResourceInfo> dateResources = getResourceListFromDirectory( dateDirFile.getAbsolutePath(), startDate, endDate); resourceList.addAll(dateResources); } } Timber.d("Found %d resources in TF card time range", resourceList.size()); return resourceList; } else { // ä¸ä½¿ç¨TFå¡ï¼æ«æåºç¨ç®å½ return getResourceListFromDirectory(outputFileDirectory, startDate, endDate); } } catch (Exception e) { Timber.e(e, "Error getting resource list"); return resourceList; } } /** * 仿å®ç®å½æ«æH264æä»¶å¹¶åå»ºèµæºå表 * @param directoryPath ç®å½è·¯å¾ * @param startDate å¼å§æ¥æ * @param endDate ç»ææ¥æ * @return èµæºå表 */ private List<ResourceInfo> getResourceListFromDirectory(String directoryPath, Date startDate, Date endDate) { List<ResourceInfo> resourceList = new ArrayList<>(); try { File dir = new File(directoryPath); if (!dir.exists() || !dir.isDirectory()) { Timber.w("Directory does not exist: %s", directoryPath); return resourceList; } File[] files = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".h264")); if (files == null || files.length == 0) { Timber.d("No H264 files found in directory: %s", directoryPath); return resourceList; } @@ -695,13 +920,57 @@ } } Timber.d("Found %d resources in time range", resourceList.size()); return resourceList; } catch (Exception e) { Timber.e(e, "Error getting resource list"); Timber.e(e, "Error getting resource list from directory: %s", directoryPath); return resourceList; } } /** * è·åæ¥æèå´å çæææ¥ææä»¶å¤¹åç§°åè¡¨ï¼æ ¼å¼ï¼yyyyMMddï¼ * @param startDate å¼å§æ¥æ * @param endDate ç»ææ¥æ * @return æ¥ææä»¶å¤¹åç§°å表 */ private List<String> getDateDirectoriesInRange(Date startDate, Date endDate) { List<String> dateDirs = new ArrayList<>(); try { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); Calendar calendar = Calendar.getInstance(); calendar.setTime(startDate); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); Date currentDate = calendar.getTime(); Date endDateOnly = new Date(endDate.getTime()); Calendar endCalendar = Calendar.getInstance(); endCalendar.setTime(endDateOnly); endCalendar.set(Calendar.HOUR_OF_DAY, 23); endCalendar.set(Calendar.MINUTE, 59); endCalendar.set(Calendar.SECOND, 59); endCalendar.set(Calendar.MILLISECOND, 999); endDateOnly = endCalendar.getTime(); // éåä»å¼å§æ¥æå°ç»ææ¥æçæææ¥æ while (!currentDate.after(endDateOnly)) { String dateDir = dateFormat.format(currentDate); dateDirs.add(dateDir); // å¢å ä¸å¤© calendar.add(Calendar.DAY_OF_MONTH, 1); currentDate = calendar.getTime(); } } catch (Exception e) { Timber.e(e, "Error getting date directories in range"); } return dateDirs; } /** @@ -711,9 +980,29 @@ * 示ä¾ï¼{"plateNumber":"京A12345","student":"å¼ ä¸","coach":"æå", * "longitude":116.397128,"latitude":39.916527,"drivingSchool":"XXé©¾æ ¡","speed":60.5} */ private void setWatermarkInfo(String watermarkInfoJson) { private void setWatermarkInfo(String watermarkInfoJson,String jsonConfig) { Timber.d("setWatermarkInfo called, watermarkInfoJson: %s", watermarkInfoJson); // è§£æ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) { // 忽ç¥è§£æé误ï¼ç»§ç»ä½¿ç¨å½åè¿ç¨ Timber.w(e, "Failed to parse cameraId from jsonConfig"); } } // 妿æå®äºcameraId=2ï¼è·¯ç±å°ç¬¬äºä¸ªè¿ç¨ if (cameraId != null && cameraId == 2) { setWatermarkInfoInProcess2(watermarkInfoJson, jsonConfig); return; } try { if (watermarkInfoJson == null || watermarkInfoJson.trim().isEmpty()) { Timber.w("Watermark info JSON is null or empty, clearing watermark"); app/src/main/java/com/anyun/h264/H264EncodeService2.java
@@ -8,6 +8,7 @@ import com.anyun.h264.model.ResourceInfo; import com.anyun.h264.model.WatermarkInfo; import com.anyun.h264.util.FileUtil; import org.json.JSONException; import org.json.JSONObject; @@ -16,6 +17,7 @@ import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; @@ -31,7 +33,7 @@ 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; @@ -47,15 +49,15 @@ 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); public List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) throws RemoteException { return H264EncodeService2.this.getResourceList(startTime, endTime,useTFCard,jsonConfig); } @Override public void setWatermarkInfo(String watermarkInfo) throws RemoteException { H264EncodeService2.this.setWatermarkInfo(watermarkInfo); public void setWatermarkInfo(String watermarkInfo,String jsonConfig) throws RemoteException { H264EncodeService2.this.setWatermarkInfo(watermarkInfo,jsonConfig); } }; @@ -102,6 +104,7 @@ int height; int framerate; String simPhone; boolean useTFCard = false; // æ¯å¦ä½¿ç¨TFå¡ // ä»JSONè§£æé ç½® static EncodeConfig fromJson(String jsonConfig) throws JSONException { @@ -114,6 +117,7 @@ config.ip = null; config.port = 0; config.simPhone = null; config.useTFCard = false; // é»è®¤ä¸ä½¿ç¨TFå¡ return config; } @@ -124,6 +128,7 @@ config.ip = json.optString("ip", null); config.port = json.optInt("port", 0); config.simPhone = json.optString("simPhone", null); config.useTFCard = json.optBoolean("useTFCard", false); return config; } @@ -216,10 +221,61 @@ } /** * è·åè¾åºæä»¶ç®å½ï¼æ ¹æ®useTFCardé ç½®ï¼ * @param useTFCard æ¯å¦ä½¿ç¨TFå¡ * @return è¾åºç®å½è·¯å¾ */ private String getOutputFileDirectory(boolean useTFCard) { if (useTFCard) { // 使ç¨TFå¡ï¼/sdcard/h264/å½åæ¥æ/ try { String storagePath = FileUtil.getStoragePath(this, true); if (storagePath == null || storagePath.trim().isEmpty()) { Timber.w("TF card storage path not available, fallback to app directory (camera2)"); return outputFileDirectory; } File externalStorage = new File(storagePath); if (!externalStorage.exists()) { Timber.w("TF card storage directory does not exist: %s, fallback to app directory (camera2)", storagePath); return outputFileDirectory; } // è·åå½åæ¥æï¼æ ¼å¼ï¼yyyyMMddï¼å¦20250123ï¼ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); String dateDir = dateFormat.format(new Date()); // æå»ºè·¯å¾ï¼/sdcard/h264/20250123/ File h264Dir = new File(externalStorage, "h264"); File dateDirFile = new File(h264Dir, dateDir); // å建ç®å½ï¼å¦æä¸åå¨ï¼ if (!dateDirFile.exists()) { boolean created = dateDirFile.mkdirs(); if (!created && !dateDirFile.exists()) { Timber.e("Failed to create TF card directory: %s, fallback to app directory (camera2)", dateDirFile.getAbsolutePath()); return outputFileDirectory; } } String tfCardPath = dateDirFile.getAbsolutePath(); Timber.d("Using TF card directory (camera2): %s", tfCardPath); return tfCardPath; } catch (Exception e) { Timber.e(e, "Error getting TF card directory, fallback to app directory (camera2)"); return outputFileDirectory; } } else { // 使ç¨åºç¨å¤é¨åå¨ç®å½ return outputFileDirectory; } } /** * å¯å¨æä»¶ç¼ç 模å¼ï¼åªåå ¥æä»¶ï¼ä¸è¿è¡ç½ç»æ¨éï¼ */ private int startFileEncode(EncodeConfig config) { Timber.d("Starting file encode mode (camera2)"); Timber.d("Starting file encode mode (camera2), useTFCard: %b", config != null ? config.useTFCard : false); // 妿ç¼ç å¨å·²ç»å¨è¿è¡ï¼å 忢 if (h264Encoder != null) { @@ -231,16 +287,22 @@ // å建ç¼ç å¨ h264Encoder = new H264Encoder(); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ // 设置 Contextï¼ç¨äºæ¸ ç TF å¡æä»¶ï¼ h264Encoder.setContext(this); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ 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); // è·åè¾åºæä»¶ç®å½ï¼æ ¹æ®useTFCardé ç½®ï¼ boolean useTFCard = config != null && config.useTFCard; String outputDir = getOutputFileDirectory(useTFCard); // 设置è¾åºæä»¶ç®å½ï¼H264Encoderä¼èªå¨ç®¡çæä»¶åå»ºï¼æ¯åéä¸ä¸ªæä»¶ï¼ // 使ç¨ä¸ä¸ªä¸´æ¶æä»¶åæ¥è®¾ç½®ç®å½ï¼H264Encoderä¼å¨åå§åæ¶å建第ä¸ä¸ªæä»¶ File tempFile = new File(outputFileDirectory, "temp.h264"); File tempFile = new File(outputDir, "temp.h264"); h264Encoder.setOutputFile(tempFile.getAbsolutePath()); h264Encoder.setEnableFileOutput(true); // å¯ç¨æä»¶è¾åº @@ -293,15 +355,24 @@ // å建ç¼ç å¨ h264Encoder = new H264Encoder(); // 设置 Contextï¼ç¨äºæ¸ ç TF å¡æä»¶ï¼ h264Encoder.setContext(this); // 设置ç¼ç åæ°ï¼ä½¿ç¨é ç½®ä¸çåæ°ï¼ 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); // è·åè¾åºæä»¶ç®å½ï¼æ ¹æ®useTFCardé ç½®ï¼ boolean useTFCard = config != null && config.useTFCard; String outputDir = getOutputFileDirectory(useTFCard); // 设置è¾åºæä»¶ç®å½ï¼H264Encoderä¼èªå¨ç®¡çæä»¶åå»ºï¼æ¯åéä¸ä¸ªæä»¶ï¼ // 使ç¨ä¸ä¸ªä¸´æ¶æä»¶åæ¥è®¾ç½®ç®å½ï¼H264Encoderä¼å¨åå§åæ¶å建第ä¸ä¸ªæä»¶ File tempFile = new File(outputFileDirectory, "temp.h264"); File tempFile = new File(outputDir, "temp.h264"); h264Encoder.setOutputFile(tempFile.getAbsolutePath()); h264Encoder.setEnableFileOutput(true); // å¯ç¨æä»¶è¾åº @@ -387,11 +458,49 @@ } try { // æ£æ¥æä»¶æ¯å¦åå¨ File file = new File(config.filePath); // è§£æå¾ ä¼ è¾æä»¶è·¯å¾ï¼è¥ä¸åå¨åå°è¯å°TFå¡ç®å½ææ¥ææ¥æ¾ String resolvedFilePath = config.filePath; File file = new File(resolvedFilePath); if (!file.exists() || !file.isFile()) { Timber.e("File does not exist: %s (camera2)", config.filePath); return 1; // 失败 Timber.w("File does not exist, try TF card lookup (camera2): %s", resolvedFilePath); String fileName = file.getName(); String timestampStr = null; // camera2çæä»¶åæ ¼å¼ï¼h264_camera2_1234567890123.h264 if (fileName.startsWith("h264_camera2_") && fileName.endsWith(".h264")) { timestampStr = fileName.substring(13, fileName.length() - 5); // 廿 "h264_camera2_" å ".h264" } if (timestampStr == null || timestampStr.trim().isEmpty()) { Timber.e("Cannot parse timestamp from file name (camera2): %s", fileName); return 1; // 失败 } try { long timestamp = Long.parseLong(timestampStr); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); String dateDir = dateFormat.format(new Date(timestamp)); String storagePath = FileUtil.getStoragePath(this, true); if (storagePath == null || storagePath.trim().isEmpty()) { Timber.e("TF card storage path not available when searching file (camera2)"); return 1; // 失败 } File tfRoot = new File(storagePath, "h264"); File candidate = new File(new File(tfRoot, dateDir), fileName); if (candidate.exists() && candidate.isFile()) { resolvedFilePath = candidate.getAbsolutePath(); file = candidate; Timber.i("Found file on TF card (camera2): %s", resolvedFilePath); } else { Timber.e("File not found on TF card path (camera2): %s", candidate.getAbsolutePath()); return 1; // 失败 } } catch (NumberFormatException e) { Timber.e(e, "Failed to parse timestamp from file name (camera2): %s", fileName); return 1; // 失败 } } // å建æä»¶ä¼ è¾å¨ @@ -440,10 +549,10 @@ } // å¼å§ä¼ è¾æä»¶ h264FileTransmitter.transmitFile(config.filePath); h264FileTransmitter.transmitFile(resolvedFilePath); Timber.d("File transmit started successfully (camera2), file: %s, server: %s:%d, protocol: %s, framerate: %d", config.filePath, config.ip, config.port, resolvedFilePath, config.ip, config.port, config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP", framerate); return 0; // æå @@ -487,32 +596,99 @@ /** * è·åèµæºåè¡¨ï¼æ ¹æ®JT/T 1076-2016表23å®ä¹ï¼ */ private List<ResourceInfo> getResourceList(String startTime, String endTime) { Timber.d("getResourceList called (camera2), startTime: %s, endTime: %s", startTime, endTime); private List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) { Timber.d("getResourceList called (camera2), startTime: %s, endTime: %s, useTFCard: %b", startTime, endTime, useTFCard); 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; } if (useTFCard) { // 使ç¨TFå¡ï¼æ«æTFå¡ä¸çh264æä»¶å¤¹ï¼æ ¹æ®æ¥æèå´è¿æ»¤ String storagePath = FileUtil.getStoragePath(this, true); if (storagePath == null || storagePath.trim().isEmpty()) { Timber.w("TF card storage path not available, fallback to app directory"); // åéå°åºç¨ç®å½ return getResourceListFromDirectory(outputFileDirectory, startDate, endDate, true); } File externalStorage = new File(storagePath); if (!externalStorage.exists()) { Timber.w("TF card storage directory does not exist: %s, fallback to app directory", storagePath); // åéå°åºç¨ç®å½ return getResourceListFromDirectory(outputFileDirectory, startDate, endDate, true); } // TFå¡ä¸çh264æä»¶å¤¹è·¯å¾ï¼/sdcard/h264/ File h264Dir = new File(externalStorage, "h264"); if (!h264Dir.exists() || !h264Dir.isDirectory()) { Timber.w("TF card h264 directory does not exist: %s", h264Dir.getAbsolutePath()); return resourceList; } // è·åæ¥æèå´å çæææ¥ææä»¶å¤¹ List<String> dateDirs = getDateDirectoriesInRange(startDate, endDate); Timber.d("Found %d date directories in range", dateDirs.size()); // æ«ææ¯ä¸ªæ¥ææä»¶å¤¹ä¸çh264æä»¶ï¼åªæ¥æ¾camera2çæä»¶ï¼ for (String dateDir : dateDirs) { File dateDirFile = new File(h264Dir, dateDir); if (dateDirFile.exists() && dateDirFile.isDirectory()) { List<ResourceInfo> dateResources = getResourceListFromDirectory( dateDirFile.getAbsolutePath(), startDate, endDate, true); resourceList.addAll(dateResources); } } Timber.d("Found %d resources for camera2 in TF card time range", resourceList.size()); return resourceList; } else { // ä¸ä½¿ç¨TFå¡ï¼æ«æåºç¨ç®å½ï¼åªæ¥æ¾camera2çæä»¶ï¼ return getResourceListFromDirectory(outputFileDirectory, startDate, endDate, true); } } catch (Exception e) { Timber.e(e, "Error getting resource list (camera2)"); return resourceList; } } /** * 仿å®ç®å½æ«æH264æä»¶å¹¶åå»ºèµæºå表 * @param directoryPath ç®å½è·¯å¾ * @param startDate å¼å§æ¥æ * @param endDate ç»ææ¥æ * @param camera2Only æ¯å¦åªæ¥æ¾camera2çæä»¶ * @return èµæºå表 */ private List<ResourceInfo> getResourceListFromDirectory(String directoryPath, Date startDate, Date endDate, boolean camera2Only) { List<ResourceInfo> resourceList = new ArrayList<>(); try { File dir = new File(directoryPath); if (!dir.exists() || !dir.isDirectory()) { Timber.w("Directory does not exist: %s", directoryPath); return resourceList; } File[] files = dir.listFiles((dir1, name) -> { boolean isH264 = name.toLowerCase().endsWith(".h264"); if (camera2Only) { return isH264 && name.contains("camera2"); } else { return isH264; } }); if (files == null || files.length == 0) { Timber.d("No H264 files found in directory: %s", directoryPath); return resourceList; } @@ -524,19 +700,63 @@ } } 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)"); Timber.e(e, "Error getting resource list from directory: %s", directoryPath); return resourceList; } } /** * è·åæ¥æèå´å çæææ¥ææä»¶å¤¹åç§°åè¡¨ï¼æ ¼å¼ï¼yyyyMMddï¼ * @param startDate å¼å§æ¥æ * @param endDate ç»ææ¥æ * @return æ¥ææä»¶å¤¹åç§°å表 */ private List<String> getDateDirectoriesInRange(Date startDate, Date endDate) { List<String> dateDirs = new ArrayList<>(); try { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); Calendar calendar = Calendar.getInstance(); calendar.setTime(startDate); calendar.set(Calendar.HOUR_OF_DAY, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); Date currentDate = calendar.getTime(); Date endDateOnly = new Date(endDate.getTime()); Calendar endCalendar = Calendar.getInstance(); endCalendar.setTime(endDateOnly); endCalendar.set(Calendar.HOUR_OF_DAY, 23); endCalendar.set(Calendar.MINUTE, 59); endCalendar.set(Calendar.SECOND, 59); endCalendar.set(Calendar.MILLISECOND, 999); endDateOnly = endCalendar.getTime(); // éåä»å¼å§æ¥æå°ç»ææ¥æçæææ¥æ while (!currentDate.after(endDateOnly)) { String dateDir = dateFormat.format(currentDate); dateDirs.add(dateDir); // å¢å ä¸å¤© calendar.add(Calendar.DAY_OF_MONTH, 1); currentDate = calendar.getTime(); } } catch (Exception e) { Timber.e(e, "Error getting date directories in range"); } return dateDirs; } /** * 设置水å°ä¿¡æ¯ */ private void setWatermarkInfo(String watermarkInfoJson) { private void setWatermarkInfo(String watermarkInfoJson,String jsonConfig) { Timber.d("setWatermarkInfo called (camera2), watermarkInfoJson: %s", watermarkInfoJson); try { @@ -635,7 +855,7 @@ if (fileName.startsWith("h264_camera2_") && fileName.endsWith(".h264")) { try { // æåæä»¶åä¸çæ¶é´æ³ String timestampStr = fileName.substring(14, fileName.length() - 5); // 廿 "h264_camera2_" å ".h264" String timestampStr = fileName.substring(13, fileName.length() - 5); // 廿 "h264_camera2_" å ".h264" long timestamp = Long.parseLong(timestampStr); startTimeFromFileName = new Date(timestamp); } catch (NumberFormatException e) { app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -1,11 +1,13 @@ package com.anyun.h264; import android.content.Context; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import com.anyun.libusbcamera.UsbCamera; import com.anyun.libusbcamera.WatermarkParam; import com.anyun.h264.model.WatermarkInfo; import com.anyun.h264.util.FileUtil; import java.io.File; import java.io.FileOutputStream; @@ -82,6 +84,10 @@ private int cameraId = 1; // æå头IDï¼é»è®¤ä¸º1ï¼ç¬¬ä¸ä¸ªæåå¤´ï¼ private long currentFileStartTime = 0; // å½åæä»¶çå¼å§æ¶é´ï¼æ¯«ç§ï¼ private static final long FILE_DURATION_MS = 60 * 1000; // æä»¶æ¶é¿ï¼1åéï¼æ¯«ç§ï¼ // Context 忏 çé ç½® private Context context; // Context 对象ï¼ç¨äºæ¸ ç TF 塿件 private long maxH264TotalSizeGB = 100; // æå¤§ H264 æä»¶æ»å¤§å°ï¼GBï¼ï¼é»è®¤ 100GB // ç½ç»ä¼ è¾æ§å¶ private boolean enableNetworkTransmission = true; // æ¯å¦å¯ç¨TCP/UDPç½ç»ä¼ è¾ @@ -158,6 +164,22 @@ } /** * 设置 Contextï¼ç¨äºæ¸ ç TF å¡æä»¶ï¼ * @param context Context 对象 */ public void setContext(Context context) { this.context = context; } /** * 设置æå¤§ H264 æä»¶æ»å¤§å°ï¼GBï¼ * @param maxTotalSizeGB æå¤§æ»å¤§å°ï¼GBï¼ï¼é»è®¤ 5GB */ public void setMaxH264TotalSizeGB(long maxTotalSizeGB) { this.maxH264TotalSizeGB = maxTotalSizeGB; } /** * 设置æ¯å¦å¯ç¨æä»¶è¾åº * @param enable true表示å¯ç¨æä»¶è¾åºï¼false表示ç¦ç¨ */ @@ -173,6 +195,18 @@ public void setEnableNetworkTransmission(boolean enable) { this.enableNetworkTransmission = enable; Timber.d("Network transmission " + (enable ? "enabled" : "disabled")); // 妿å¨ç¼ç è¿ç¨ä¸å¨æå¼å¯ç½ç»ä¼ è¾ï¼éè¦ç¡®ä¿åºå±Socketå·²ç»å»ºç« if (enable) { if (!protocolHelper.initializeSocket()) { Timber.e("Failed to initialize socket when enabling network transmission"); } } else { // å¨æå ³éç½ç»ä¼ è¾æ¶ï¼åæ¶éæ¾åºå±Socketèµæº if (protocolHelper != null) { protocolHelper.closeSocket(); } } } /** @@ -286,6 +320,7 @@ } break; } else { usbCamera.stopCamera(); // 失败ï¼è®°å½æ¥å¿ Timber.w( "prepareCamera failed on attempt " + (attempt + 1) + ": " + result); if (attempt < maxRetries - 1) { @@ -417,6 +452,25 @@ Timber.e(e, "Error closing previous file"); } fileOutputStream = null; } // æ£æ¥å¹¶æ¸ ç TF å¡ä¸ç h264 æä»¶ï¼å¦æéè¦ï¼ if (context != null && outputFileDirectory != null && !outputFileDirectory.isEmpty()) { try { // 夿å½åç®å½æ¯å¦å¨ TF å¡ç h264 ç®å½ä¸ // ç®å½ç»æï¼/sdcard/h264/yyyyMMdd/ï¼h264 æ ¹ç®å½åºè¯¥æ¯ç¶ç®å½çç¶ç®å½ File currentDir = new File(outputFileDirectory); File parentDir = currentDir.getParentFile(); if (parentDir != null && "h264".equals(parentDir.getName())) { // å½åç®å½å¨ h264 ç®å½ä¸ï¼è·å h264 æ ¹ç®å½ String h264RootDir = parentDir.getAbsolutePath(); Timber.d("Checking and cleaning up h264 files in: %s", h264RootDir); FileUtil.cleanupH264Files(context, h264RootDir, maxH264TotalSizeGB, 1); } } catch (Exception e) { Timber.e(e, "Error during h264 files cleanup check"); // æ¸ ç失败ä¸å½±åæä»¶å建ï¼ç»§ç»æ§è¡ } } // çææ°æä»¶å @@ -774,8 +828,8 @@ // å¤å¶ IDR 帧 System.arraycopy(idr, 0, combined, offset, idr.length); Timber.d("Combined SPS/PPS/IDR frame, total size: %d (SPS: %d, PPS: %d, IDR: %d)", totalLength, sps.length, pps.length, idr.length); // Timber.d("Combined SPS/PPS/IDR frame, total size: %d (SPS: %d, PPS: %d, IDR: %d)", // totalLength, sps.length, pps.length, idr.length); return combined; } app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java
@@ -55,6 +55,8 @@ // TCPåæ° private JT1076TcpClient tcpClient; // æ§å¶æªè¿æ¥æ¥å¿çè¾åºé¢çï¼é¿å å·å± private boolean tcpNotConnectedLogged = false; // RTPåè®®åæ° private String simCardNumber = "123456789012"; // 12ä½SIMå¡å· @@ -162,6 +164,8 @@ @Override public void onConnected() { Timber.d("TCP connection established"); // è¿æ¥æååï¼å è®¸ä¸æ¬¡æå¼æ¶å次æå°æªè¿æ¥åè¦ tcpNotConnectedLogged = false; } @Override @@ -255,8 +259,14 @@ public void sendTcpPacket(byte[] packet) { if (tcpClient != null && tcpClient.isConnected()) { tcpClient.sendPacket(packet); // åéæåï¼éç½®æªè¿æ¥æ¥å¿æ è®° tcpNotConnectedLogged = false; } else { Timber.w("TCP socket not connected"); // ä» å¨ç¬¬ä¸æ¬¡æ£æµå°æªè¿æ¥æ¶æå°warnï¼é¿å æ¥å¿å·å± if (!tcpNotConnectedLogged) { Timber.w("TCP socket not connected"); tcpNotConnectedLogged = true; } } } app/src/main/java/com/anyun/h264/model/ResourceInfo.java
@@ -155,3 +155,6 @@ } } app/src/main/java/com/anyun/h264/model/WatermarkInfo.java
@@ -138,3 +138,5 @@ } } app/src/main/java/com/anyun/h264/service/H264EncodeServiceClient.java
@@ -191,14 +191,14 @@ * @param endTime ç»ææ¶é´ï¼æ ¼å¼ï¼YYMMDDHHmmssï¼ä¾å¦ï¼240101235959ï¼ * @return èµæºå表ï¼å¦æå¤±è´¥è¿ånull */ public List<ResourceInfo> getResourceList(String startTime, String endTime) { public List<ResourceInfo> getResourceList(String startTime, String endTime,boolean useTFCard,String jsonConfig) { if (!isServiceBound()) { Log.e(TAG, "Service is not bound"); return null; } try { List<ResourceInfo> result = service.getResourceList(startTime, endTime); List<ResourceInfo> result = service.getResourceList(startTime, endTime,useTFCard,jsonConfig); Log.d(TAG, "getResourceList returned " + (result != null ? result.size() : 0) + " resources"); return result; } catch (RemoteException e) { @@ -214,14 +214,14 @@ * å¦æä¼ å ¥nullæç©ºå符串ï¼å°æ¸ 餿°´å° * @return true-æåï¼false-失败 */ public boolean setWatermarkInfo(String watermarkInfo) { public boolean setWatermarkInfo(String watermarkInfo,String jsonConfig) { if (!isServiceBound()) { Log.e(TAG, "Service is not bound"); return false; } try { service.setWatermarkInfo(watermarkInfo); service.setWatermarkInfo(watermarkInfo,jsonConfig); Log.d(TAG, "setWatermarkInfo called with: " + watermarkInfo); return true; } catch (RemoteException e) { app/src/main/java/com/anyun/h264/util/FileUtil.java
New file @@ -0,0 +1,249 @@ package com.anyun.h264.util; import android.content.Context; import android.os.storage.StorageManager; import android.os.StatFs; import java.io.File; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; import timber.log.Timber; public class FileUtil { //è·åæå ¥çTFCardç®å½è·¯å¾ public static String getStoragePath(Context mContext, boolean is_removale) { if (mContext != null) { StorageManager mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); Class<?> storageVolumeClazz = null; try { storageVolumeClazz = Class.forName("android.os.storage.StorageVolume"); Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList"); Method getPath = storageVolumeClazz.getMethod("getPath"); Method isRemovable = storageVolumeClazz.getMethod("isRemovable"); Object result = getVolumeList.invoke(mStorageManager); final int length = Array.getLength(result); for (int i = 0; i < length; i++) { Object storageVolumeElement = Array.get(result, i); String path = (String) getPath.invoke(storageVolumeElement); boolean removable = (Boolean) isRemovable.invoke(storageVolumeElement); if (is_removale == removable) { return path; } } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } return null; } else { return null; } } /** * æ¸ ç TF å¡ä¸ç h264 æä»¶ * å½ h264 æä»¶æ»å¤§å°è¶ è¿æå®é弿 TF å¡å©ä½ç©ºé´å°äºæå®å¼æ¶ï¼å 餿¥æææ©çæä»¶å¤¹ * * @param context Context 对象ï¼ç¨äºè·å TF å¡è·¯å¾åå©ä½ç©ºé´ * @param h264RootDir h264 æ ¹ç®å½è·¯å¾ï¼ä¾å¦ "/sdcard/h264" * @param maxTotalSizeGB æå¤§æ»å¤§å°ï¼GBï¼ï¼é»è®¤ 5GB * @param minFreeSpaceGB æå°å©ä½ç©ºé´ï¼GBï¼ï¼é»è®¤ 1GB */ public static void cleanupH264Files(Context context, String h264RootDir, long maxTotalSizeGB, long minFreeSpaceGB) { if (context == null || h264RootDir == null || h264RootDir.trim().isEmpty()) { Timber.w("Context or h264 root directory is null, skip cleanup"); return; } File h264Root = new File(h264RootDir); if (!h264Root.exists() || !h264Root.isDirectory()) { Timber.d("H264 root directory does not exist: %s, skip cleanup", h264RootDir); return; } try { // è·å TF å¡è·¯å¾ String tfCardPath = getStoragePath(context, true); if (tfCardPath == null || tfCardPath.trim().isEmpty()) { Timber.w("TF card path not available, skip cleanup"); return; } // è·å TF å¡å©ä½ç©ºé´ï¼GBï¼ long freeSpaceGB = getFreeSpaceGB(tfCardPath); Timber.d("TF card free space: %d GB", freeSpaceGB); // æ«ææææ¥ææä»¶å¤¹ File[] dateDirs = h264Root.listFiles(File::isDirectory); if (dateDirs == null || dateDirs.length == 0) { Timber.d("No date directories found in h264 root: %s", h264RootDir); return; } // ææ¥ææåºï¼ææ©çå¨åï¼ List<DateDirInfo> dateDirList = new ArrayList<>(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd", Locale.CHINA); for (File dateDir : dateDirs) { String dirName = dateDir.getName(); // åªå¤çç¬¦åæ¥ææ ¼å¼çæä»¶å¤¹ï¼yyyyMMddï¼ if (dirName.length() == 8 && dirName.matches("\\d{8}")) { try { Date date = dateFormat.parse(dirName); long totalSize = calculateH264FilesSize(dateDir); dateDirList.add(new DateDirInfo(dateDir, date, totalSize)); } catch (ParseException e) { Timber.w("Invalid date directory name: %s", dirName); } } } // ææ¥ææåºï¼ææ©çå¨åï¼ Collections.sort(dateDirList, new Comparator<DateDirInfo>() { @Override public int compare(DateDirInfo o1, DateDirInfo o2) { return o1.date.compareTo(o2.date); } }); // è®¡ç®ææ h264 æä»¶çæ»å¤§å°ï¼GBï¼ long totalSizeGB = 0; for (DateDirInfo info : dateDirList) { totalSizeGB += info.totalSize / (1024L * 1024L * 1024L); } Timber.d("Total h264 files size: %d GB, Max allowed: %d GB", totalSizeGB, maxTotalSizeGB); Timber.d("TF card free space: %d GB, Min required: %d GB", freeSpaceGB, minFreeSpaceGB); // æ£æ¥æ¯å¦éè¦æ¸ ç boolean needCleanup = (totalSizeGB > maxTotalSizeGB) || (freeSpaceGB < minFreeSpaceGB); if (!needCleanup) { Timber.d("No cleanup needed"); return; } // å 餿æ©çæ¥ææä»¶å¤¹ï¼ç´å°æ»¡è¶³æ¡ä»¶ int deletedCount = 0; while (!dateDirList.isEmpty() && ((totalSizeGB > maxTotalSizeGB) || (freeSpaceGB < minFreeSpaceGB))) { DateDirInfo oldestDir = dateDirList.remove(0); Timber.d("Deleting oldest date directory: %s (size: %d GB, date: %s)", oldestDir.dir.getName(), oldestDir.totalSize / (1024L * 1024L * 1024L), dateFormat.format(oldestDir.date)); // å 餿件夹åå ¶ææå 容 if (deleteDirectory(oldestDir.dir)) { deletedCount++; totalSizeGB -= oldestDir.totalSize / (1024L * 1024L * 1024L); // éæ°è·åå©ä½ç©ºé´ freeSpaceGB = getFreeSpaceGB(tfCardPath); Timber.d("After deletion - Total size: %d GB, Free space: %d GB", totalSizeGB, freeSpaceGB); } else { Timber.e("Failed to delete directory: %s", oldestDir.dir.getAbsolutePath()); break; // å é¤å¤±è´¥ï¼åæ¢æ¸ ç } } Timber.i("Cleanup completed. Deleted %d date directories. Final total size: %d GB, Free space: %d GB", deletedCount, totalSizeGB, freeSpaceGB); } catch (Exception e) { Timber.e(e, "Error during h264 files cleanup"); } } /** * æ¸ ç TF å¡ä¸ç h264 æä»¶ï¼ä½¿ç¨é»è®¤åæ°ï¼æå¤§5GBï¼æå°å©ä½ç©ºé´1GBï¼ */ public static void cleanupH264Files(Context context, String h264RootDir) { cleanupH264Files(context, h264RootDir, 5, 1); } /** * 计ç®ç®å½ä¸ææ .h264 æä»¶çæ»å¤§å°ï¼åèï¼ */ private static long calculateH264FilesSize(File dir) { long totalSize = 0; File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.getName().toLowerCase().endsWith(".h264")) { totalSize += file.length(); } } } return totalSize; } /** * è·åæå®è·¯å¾çå©ä½ç©ºé´ï¼GBï¼ */ private static long getFreeSpaceGB(String path) { try { StatFs statFs = new StatFs(path); long blockSize = statFs.getBlockSizeLong(); long availableBlocks = statFs.getAvailableBlocksLong(); long freeBytes = availableBlocks * blockSize; return freeBytes / (1024L * 1024L * 1024L); // 转æ¢ä¸º GB } catch (Exception e) { Timber.e(e, "Error getting free space for path: %s", path); return 0; } } /** * éå½å é¤ç®å½åå ¶ææå 容 */ private static boolean deleteDirectory(File dir) { if (dir == null || !dir.exists()) { return true; } if (dir.isDirectory()) { File[] children = dir.listFiles(); if (children != null) { for (File child : children) { if (!deleteDirectory(child)) { return false; } } } } return dir.delete(); } /** * æ¥ææä»¶å¤¹ä¿¡æ¯ */ private static class DateDirInfo { File dir; Date date; long totalSize; // åè DateDirInfo(File dir, Date date, long totalSize) { this.dir = dir; this.date = date; this.totalSize = totalSize; } } } app/src/main/res/values/strings.xml
@@ -1,3 +1,3 @@ <resources> <string name="app_name">My Application</string> <string name="app_name">HuBeiVideo</string> </resources> check_h264.py
@@ -207,3 +207,6 @@ sys.exit(0 if success else 1) ¶à½ø³Ì·½°¸Ê¹ÓÃ˵Ã÷.md
@@ -115,3 +115,6 @@ - 第äºä¸ªè¿ç¨éè¿`android:process=":camera2"`é ç½®å¨ç¬ç«è¿ç¨ä¸è¿è¡ - æææä½é½éè¿AIDLæ¥å£è¿è¡è¿ç¨é´éä¿¡ ÈçºÎ¼ì²étest.h264Îļþ.md
@@ -182,3 +182,5 @@ 3. **ç¨VLCææ¾**éªè¯ 4. **å¦æè¿æé®é¢**ï¼è¿è¡æ£æ¥å·¥å ·è·å详ç»è¯æ