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; 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; import java.io.File; 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; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * H264编码服务 * 提供AIDL接口供客户端调用,用于控制H264编码和查询资源列表 */ public class H264EncodeService extends Service { private static final String TAG = "H264EncodeService"; private H264Encoder h264Encoder; private H264FileTransmitter h264FileTransmitter; // H264文件传输器 private String outputFileDirectory; // H264文件输出目录 private WatermarkInfo currentWatermarkInfo; // 当前水印信息 private static final int H264_FILE_RETENTION_DAYS = 1; // 可根据需求调整为3或5天 // 多进程支持:第二个摄像头的服务连接 private IH264EncodeService camera2Service; private ServiceConnection camera2Connection; private boolean isCamera2Bound = false; // 默认编码参数 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 // 默认摄像头参数 private static final int[] DEFAULT_CAMERA_ID_RANGE = {1, 2}; private static final int[] DEFAULT_RESOLUTION = {640, 480}; // AIDL接口实现 private final IH264EncodeService.Stub binder = new IH264EncodeService.Stub() { @Override public int controlEncode(int action, String jsonConfig) throws RemoteException { return H264EncodeService.this.controlEncode(action, jsonConfig); } @Override public List 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,String jsonConfig) throws RemoteException { H264EncodeService.this.setWatermarkInfo(watermarkInfo,jsonConfig); } }; @Override public void onCreate() { super.onCreate(); Timber.d("H264EncodeService created"); // 初始化输出文件目录(使用应用外部存储目录) outputFileDirectory = getExternalFilesDir(null).getAbsolutePath(); Timber.d("Output file directory: %s", outputFileDirectory); // 清理过期的H264文件 cleanupExpiredH264Files(H264_FILE_RETENTION_DAYS); } @Override public IBinder onBind(Intent intent) { Timber.d("Service bound"); return binder; } @Override public boolean onUnbind(Intent intent) { Timber.d("Service unbound"); // 不自动停止编码器,让它在服务中保持运行 return super.onUnbind(intent); } @Override public void onDestroy() { super.onDestroy(); Timber.d("Service destroyed"); // 停止并释放编码器和文件传输器 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; } } /** * 编码配置类 */ 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; int height; int framerate; String simPhone; Integer cameraId; // 摄像头ID(1或2,用于多进程方案) // 从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.enableFileOutput = false; config.enableNetworkTransmit = false; config.ip = null; config.port = 0; config.simPhone = null; config.cameraId = 1; // 默认使用第一个摄像头 config.useTFCard = false; // 默认不使用TF卡 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.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")) { config.cameraId = json.optInt("cameraId", 1); } else { config.cameraId = 1; // 默认使用第一个摄像头 } 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编码和文件传输 * @param action 操作类型: * 0-开启h264文件写入, * 1-停止h264编码并停止写入文件, * 2-开启网络推送h264(不写入文件), * 3-停止h264编码并停止网络推送, * 4-开始传输H264文件(从文件读取并网络推送), * 5-停止H264文件传输 * @param jsonConfig JSON格式的配置参数 * 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,或包含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 { EncodeConfig config0 = EncodeConfig.fromJson(jsonConfig); config0.enableFileOutput = true; config0.enableNetworkTransmit = false; return startEncode(config0); } catch (JSONException e) { Timber.e(e, "Failed to parse JSON config: %s", jsonConfig); return 1; } case 1: // 停止h264编码并停止写入文件 // 检查是否指定了cameraId=2 if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } return stopEncoder(); case 2: // 开启网络推送h264(可同时写入文件) try { EncodeConfig config2 = EncodeConfig.fromJson(jsonConfig); // 检查必需的配置参数 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: // 停止网络推送(保持文件写入) // 检查是否指定了cameraId=2 if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } // 只关闭网络传输,保持文件写入 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 { 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("客户端请求停止视频文件上传"); // 检查是否指定了cameraId=2 if (cameraId != null && cameraId == 2) { return controlEncodeInProcess2(action, jsonConfig); } return stopFileTransmitter(); default: Timber.e("Unknown action: %d", action); return 1; // 失败 } } catch (Exception e) { 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; } } /** * 在第二个进程(camera2)中获取资源列表 */ private List 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"); } } /** * 确保第二个进程的服务已绑定 */ 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]; } /** * 获取输出文件目录(根据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"); return outputFileDirectory; } 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; } } else { // 使用应用外部存储目录 return outputFileDirectory; } } /** * 启动编码(统一方法,支持文件写入和网络传输的组合) * @param config 编码配置 * @return 0-成功,1-失败 */ 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.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; // 失败 } } // 编码器未运行,需要初始化并启动 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); h264Encoder.setUseTFCard(config.useTFCard); // 设置编码参数(使用配置中的参数) 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配置) String outputDir = getOutputFileDirectory(config.useTFCard); // 设置输出文件目录(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; 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"); } h264Encoder.start(); 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"); h264Encoder = null; return 1; // 失败 } } catch (Exception e) { Timber.e(e, "Failed to start encode"); h264Encoder = null; return 1; // 失败 } } /** * 停止编码器 */ private int stopEncoder() { Timber.d("Stopping encoder"); if (h264Encoder != null) { try { h264Encoder.stop(); h264Encoder.release(); h264Encoder = null; Timber.d("Encoder stopped successfully"); return 0; // 成功 } catch (Exception e) { Timber.e(e, "Error stopping encoder"); h264Encoder = null; return 1; // 失败 } } else { Timber.w("Encoder is not running"); return 0; // 成功(没有运行的编码器,视为成功) } } /** * 启动文件传输模式(从H264文件读取并网络推送) */ private int startFileTransmit(FileTransmitConfig config) { Timber.d("Starting file transmit mode"); // 如果文件传输器已经在运行,先停止 if (h264FileTransmitter != null) { Timber.w("File transmitter is already running, 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"); return 1; // 失败 } if (config.filePath == null || config.filePath.trim().isEmpty()) { Timber.e("File transmit requires valid filePath in config"); return 1; // 失败 } try { // 解析待传输文件路径,若不存在则尝试到TF卡目录按日期查找 String resolvedFilePath = config.filePath; File file = new File(resolvedFilePath); if (!file.exists() || !file.isFile()) { 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; // 失败 } } // 创建文件传输器 h264FileTransmitter = new H264FileTransmitter(); // 设置服务器地址 h264FileTransmitter.setServerAddress(config.ip, config.port); // 设置协议类型 h264FileTransmitter.setProtocolType(1); //1-tcp // 设置协议参数(SIM卡号和逻辑通道号) String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty() ? config.simPhone : "013120122580"; h264FileTransmitter.setProtocolParams(simPhone, (byte)1); // 设置帧率(用于计算时间戳间隔) 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: frame %d%s", currentFrame, // totalFrames > 0 ? " of " + totalFrames : ""); } @Override public void onComplete() { Timber.d("File transmit completed"); stopFileTransmitter(); } @Override public void onError(String error) { Timber.e("File transmit error: %s", error); } }); // 初始化Socket连接 if (!h264FileTransmitter.initialize()) { Timber.e("Failed to initialize file transmitter socket"); h264FileTransmitter = null; return 1; // 失败 } // 开始传输文件 h264FileTransmitter.transmitFile(resolvedFilePath); Timber.d("File transmit started successfully, file: %s, server: %s:%d, protocol: %s, framerate: %d", resolvedFilePath, 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"); if (h264FileTransmitter != null) { try { h264FileTransmitter.stop(); } catch (Exception ex) { Timber.e(ex, "Error stopping file transmitter after failure"); } h264FileTransmitter = null; } return 1; // 失败 } } /** * 停止文件传输器 */ private int stopFileTransmitter() { Timber.d("Stopping file transmitter"); if (h264FileTransmitter != null) { try { h264FileTransmitter.stop(); h264FileTransmitter = null; Timber.d("File transmitter stopped successfully"); return 0; // 成功 } catch (Exception e) { Timber.e(e, "Error stopping file transmitter"); h264FileTransmitter = null; return 1; // 失败 } } else { Timber.w("File transmitter is not running"); return 0; // 成功(没有运行的文件传输器,视为成功) } } /** * 获取资源列表(根据JT/T 1076-2016表23定义) * @param startTime 开始时间(格式:YYMMDDHHmmss) * @param endTime 结束时间(格式:YYMMDDHHmmss) * @return 资源列表 */ private List 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 resourceList = new ArrayList<>(); try { // 解析时间范围 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 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 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 getResourceListFromDirectory(String directoryPath, Date startDate, Date endDate) { List 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; } // 遍历文件,查找在时间范围内的文件 for (File file : files) { ResourceInfo resourceInfo = createResourceInfoFromFile(file, startDate, endDate); if (resourceInfo != null) { resourceList.add(resourceInfo); } } return resourceList; } catch (Exception e) { Timber.e(e, "Error getting resource list from directory: %s", directoryPath); return resourceList; } } /** * 获取日期范围内的所有日期文件夹名称列表(格式:yyyyMMdd) * @param startDate 开始日期 * @param endDate 结束日期 * @return 日期文件夹名称列表 */ private List getDateDirectoriesInRange(Date startDate, Date endDate) { List 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; } /** * 设置水印信息 * @param watermarkInfoJson 水印信息JSON字符串,包含:车牌(plateNumber)、学员(student)、教练(coach)、 * 经度(longitude)、纬度(latitude)、驾校(drivingSchool)、车速(speed) * 示例:{"plateNumber":"京A12345","student":"张三","coach":"李四", * "longitude":116.397128,"latitude":39.916527,"drivingSchool":"XX驾校","speed":60.5} */ 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"); 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: %s", watermarkInfo); // 如果编码器正在运行,立即应用水印 if (h264Encoder != null) { h264Encoder.setWatermarkInfo(watermarkInfo); Timber.d("Watermark applied to encoder"); } else { Timber.d("Encoder not running, watermark will be applied when encoder starts"); } } catch (JSONException e) { Timber.e(e, "Failed to parse watermark info JSON: %s", watermarkInfoJson); currentWatermarkInfo = null; } catch (Exception e) { Timber.e(e, "Unexpected error setting watermark info"); 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()); } } } } /** * 从文件创建资源信息(如果文件在时间范围内) */ private ResourceInfo createResourceInfoFromFile(File file, Date startDate, Date endDate) { try { // 从文件名中提取时间戳(格式:h264_1234567890123.h264) String fileName = file.getName(); Date startTimeFromFileName = null; if (fileName.startsWith("h264_") && fileName.endsWith(".h264")) { try { // 提取文件名中的时间戳(h264_ 和 .h264 之间的部分) String timestampStr = fileName.substring(5, fileName.length() - 5); // 去掉 "h264_" 和 ".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()); // 检查文件时间是否在指定范围内 // 开始时间应该 >= startDate,结束时间应该 <= endDate // 如果文件的开始时间在范围内,或者结束时间在范围内,或者文件时间范围包含查询范围,则包含该文件 if (startTimeFromFileName.after(endDate) || endTimeFromFile.before(startDate)) { return null; // 不在时间范围内 } // 创建资源信息对象 ResourceInfo resourceInfo = new ResourceInfo(); // 逻辑通道号(默认值,实际应从配置获取) resourceInfo.setLogicalChannelNumber((byte) 1); // 开始时间:从文件名中的时间戳 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时间字符串 * @param timeStr 时间字符串(格式:YYMMDDHHmmss) * @return Date对象,如果解析失败返回null */ 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; } } }