| New file |
| | |
| | | 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); |
| | | |
| | | long timeFile = System.currentTimeMillis()/1000*1000; |
| | | SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss"); |
| | | String str = bcdFormat.format(timeFile); |
| | | Timber.i("文件名 (camera2):%s", str); |
| | | // 设置输出文件(添加camera2标识) |
| | | String fileName = "h264_camera2_" + timeFile+ ".h264"; |
| | | File outputFile = new File(outputFileDirectory, fileName); |
| | | h264Encoder.setOutputFile(outputFile.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 file: %s, resolution: %dx%d, framerate: %d", |
| | | outputFile.getAbsolutePath(), 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); |
| | | |
| | | long timeFile = System.currentTimeMillis()/1000*1000; |
| | | SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss"); |
| | | String str = bcdFormat.format(timeFile); |
| | | Timber.i("startNetworkEncode (camera2) 文件名:%s", str); |
| | | // 设置输出文件(添加camera2标识) |
| | | String fileName = "h264_camera2_" + timeFile+ ".h264"; |
| | | File outputFile = new File(outputFileDirectory, fileName); |
| | | h264Encoder.setOutputFile(outputFile.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; |
| | | } |
| | | } |
| | | } |
| | | |