| New file |
| | |
| | | package com.anyun.h264; |
| | | |
| | | import android.app.Service; |
| | | import android.content.Intent; |
| | | import android.os.IBinder; |
| | | import android.os.RemoteException; |
| | | import android.util.Log; |
| | | |
| | | import com.anyun.h264.model.ResourceInfo; |
| | | |
| | | 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编码服务 |
| | | * 提供AIDL接口供客户端调用,用于控制H264编码和查询资源列表 |
| | | */ |
| | | public class H264EncodeService extends Service { |
| | | private static final String TAG = "H264EncodeService"; |
| | | |
| | | private H264Encoder h264Encoder; |
| | | private String outputFileDirectory; // H264文件输出目录 |
| | | |
| | | // 默认编码参数 |
| | | 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<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException { |
| | | return H264EncodeService.this.getResourceList(startTime, endTime); |
| | | } |
| | | }; |
| | | |
| | | @Override |
| | | public void onCreate() { |
| | | super.onCreate(); |
| | | Log.d(TAG, "H264EncodeService created"); |
| | | |
| | | // 初始化输出文件目录(使用应用外部存储目录) |
| | | outputFileDirectory = getExternalFilesDir(null).getAbsolutePath(); |
| | | Log.d(TAG, "Output file directory: " + outputFileDirectory); |
| | | } |
| | | |
| | | @Override |
| | | public IBinder onBind(Intent intent) { |
| | | Log.d(TAG, "Service bound"); |
| | | return binder; |
| | | } |
| | | |
| | | @Override |
| | | public boolean onUnbind(Intent intent) { |
| | | Log.d(TAG, "Service unbound"); |
| | | // 不自动停止编码器,让它在服务中保持运行 |
| | | return super.onUnbind(intent); |
| | | } |
| | | |
| | | @Override |
| | | public void onDestroy() { |
| | | super.onDestroy(); |
| | | Log.d(TAG, "Service destroyed"); |
| | | |
| | | // 停止并释放编码器 |
| | | stopEncoder(); |
| | | } |
| | | |
| | | /** |
| | | * 编码配置类 |
| | | */ |
| | | private static class EncodeConfig { |
| | | String ip; |
| | | int port; |
| | | int width; |
| | | int height; |
| | | int framerate; |
| | | |
| | | // 从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; |
| | | 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); |
| | | |
| | | return config; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 控制H264编码 |
| | | * @param action 操作类型:0-开启h264文件写入,1-停止h264编码并停止写入文件,2-开启网络推送h264(不写入文件),3-停止h264编码并停止网络推送 |
| | | * @param jsonConfig JSON格式的配置参数,包含:ip、port、width、height、framerate |
| | | * @return 0-成功,1-失败 |
| | | */ |
| | | private synchronized int controlEncode(int action, String jsonConfig) { |
| | | Log.d(TAG, "controlEncode called with action: " + action + ", jsonConfig: " + jsonConfig); |
| | | |
| | | try { |
| | | EncodeConfig config = null; |
| | | |
| | | // 对于停止操作,不需要解析配置 |
| | | if (action != 1 && action != 3) { |
| | | try { |
| | | config = EncodeConfig.fromJson(jsonConfig); |
| | | Log.d(TAG, "Parsed config - width: " + config.width + ", height: " + config.height + |
| | | ", framerate: " + config.framerate + ", ip: " + config.ip + ", port: " + config.port); |
| | | } catch (JSONException e) { |
| | | Log.e(TAG, "Failed to parse JSON config: " + jsonConfig, e); |
| | | return 1; // 失败 |
| | | } |
| | | } |
| | | |
| | | switch (action) { |
| | | case 0: // 开启h264文件写入 |
| | | return startFileEncode(config); |
| | | |
| | | case 1: // 停止h264编码并停止写入文件 |
| | | return stopEncoder(); |
| | | |
| | | case 2: // 开启网络推送h264(不写入文件) |
| | | return startNetworkEncode(config); |
| | | |
| | | case 3: // 停止h264编码并停止网络推送 |
| | | return stopEncoder(); |
| | | |
| | | default: |
| | | Log.e(TAG, "Unknown action: " + action); |
| | | return 1; // 失败 |
| | | } |
| | | } catch (Exception e) { |
| | | Log.e(TAG, "Error in controlEncode", e); |
| | | return 1; // 失败 |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 启动文件编码模式(只写入文件,不进行网络推送) |
| | | */ |
| | | private int startFileEncode(EncodeConfig config) { |
| | | Log.d(TAG, "Starting file encode mode"); |
| | | |
| | | // 如果编码器已经在运行,先停止 |
| | | if (h264Encoder != null) { |
| | | Log.w(TAG, "Encoder is already running, stopping it first"); |
| | | stopEncoder(); |
| | | } |
| | | |
| | | try { |
| | | // 创建编码器 |
| | | h264Encoder = new H264Encoder(); |
| | | |
| | | // 设置编码参数(使用配置中的参数) |
| | | int width = config != null ? config.width : DEFAULT_WIDTH; |
| | | int height = config != null ? config.height : DEFAULT_HEIGHT; |
| | | int framerate = config != null ? config.framerate : DEFAULT_FRAME_RATE; |
| | | h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); |
| | | |
| | | // 设置输出文件 |
| | | String fileName = "h264_" + System.currentTimeMillis() + ".h264"; |
| | | File outputFile = new File(outputFileDirectory, fileName); |
| | | h264Encoder.setOutputFile(outputFile.getAbsolutePath()); |
| | | h264Encoder.setEnableFileOutput(true); // 启用文件输出 |
| | | |
| | | // 禁用网络传输 |
| | | h264Encoder.setEnableNetworkTransmission(false); |
| | | |
| | | // 初始化并启动(使用配置中的分辨率) |
| | | int[] resolution = {width, height}; |
| | | if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) { |
| | | h264Encoder.start(); |
| | | Log.d(TAG, "File encode started successfully, output file: " + outputFile.getAbsolutePath() + |
| | | ", resolution: " + width + "x" + height + ", framerate: " + framerate); |
| | | return 0; // 成功 |
| | | } else { |
| | | Log.e(TAG, "Failed to initialize encoder"); |
| | | h264Encoder = null; |
| | | return 1; // 失败 |
| | | } |
| | | } catch (Exception e) { |
| | | Log.e(TAG, "Failed to start file encode", e); |
| | | h264Encoder = null; |
| | | return 1; // 失败 |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 启动网络推送模式(只进行网络推送,不写入文件) |
| | | */ |
| | | private int startNetworkEncode(EncodeConfig config) { |
| | | Log.d(TAG, "Starting network encode mode"); |
| | | |
| | | // 如果编码器已经在运行,先停止 |
| | | if (h264Encoder != null) { |
| | | Log.w(TAG, "Encoder is already running, stopping it first"); |
| | | stopEncoder(); |
| | | } |
| | | |
| | | // 检查必需的配置参数 |
| | | if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) { |
| | | Log.e(TAG, "Network encode requires valid ip and port in config"); |
| | | return 1; // 失败 |
| | | } |
| | | |
| | | try { |
| | | // 创建编码器 |
| | | h264Encoder = new H264Encoder(); |
| | | |
| | | // 设置编码参数(使用配置中的参数) |
| | | int width = config.width; |
| | | int height = config.height; |
| | | int framerate = config.framerate; |
| | | h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE); |
| | | |
| | | // 禁用文件输出 |
| | | h264Encoder.setEnableFileOutput(false); |
| | | |
| | | // 启用网络传输并设置服务器地址 |
| | | h264Encoder.setEnableNetworkTransmission(true); |
| | | h264Encoder.setServerAddress(config.ip, config.port); |
| | | |
| | | // 设置协议参数(使用默认值,可根据需要从配置中添加) |
| | | // TODO: 如果需要在JSON配置中添加simCardNumber和logicalChannelNumber,可以在这里解析 |
| | | h264Encoder.setProtocolParams("123456789012", (byte)1); |
| | | |
| | | // 初始化并启动(使用配置中的分辨率) |
| | | int[] resolution = {width, height}; |
| | | if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) { |
| | | h264Encoder.start(); |
| | | Log.d(TAG, "Network encode started successfully, server: " + config.ip + ":" + config.port + |
| | | ", resolution: " + width + "x" + height + ", framerate: " + framerate); |
| | | return 0; // 成功 |
| | | } else { |
| | | Log.e(TAG, "Failed to initialize encoder"); |
| | | h264Encoder = null; |
| | | return 1; // 失败 |
| | | } |
| | | } catch (Exception e) { |
| | | Log.e(TAG, "Failed to start network encode", e); |
| | | h264Encoder = null; |
| | | return 1; // 失败 |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 停止编码器 |
| | | */ |
| | | private int stopEncoder() { |
| | | Log.d(TAG, "Stopping encoder"); |
| | | |
| | | if (h264Encoder != null) { |
| | | try { |
| | | h264Encoder.stop(); |
| | | h264Encoder.release(); |
| | | h264Encoder = null; |
| | | Log.d(TAG, "Encoder stopped successfully"); |
| | | return 0; // 成功 |
| | | } catch (Exception e) { |
| | | Log.e(TAG, "Error stopping encoder", e); |
| | | h264Encoder = null; |
| | | return 1; // 失败 |
| | | } |
| | | } else { |
| | | Log.w(TAG, "Encoder is not running"); |
| | | return 0; // 成功(没有运行的编码器,视为成功) |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取资源列表(根据JT/T 1076-2016表23定义) |
| | | * @param startTime 开始时间(格式:YYMMDDHHmmss) |
| | | * @param endTime 结束时间(格式:YYMMDDHHmmss) |
| | | * @return 资源列表 |
| | | */ |
| | | private List<ResourceInfo> getResourceList(String startTime, String endTime) { |
| | | Log.d(TAG, "getResourceList called, startTime: " + startTime + ", endTime: " + endTime); |
| | | |
| | | List<ResourceInfo> resourceList = new ArrayList<>(); |
| | | |
| | | try { |
| | | // 扫描输出目录中的H264文件 |
| | | File dir = new File(outputFileDirectory); |
| | | if (!dir.exists() || !dir.isDirectory()) { |
| | | Log.w(TAG, "Output directory does not exist: " + outputFileDirectory); |
| | | return resourceList; |
| | | } |
| | | |
| | | File[] files = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".h264")); |
| | | if (files == null || files.length == 0) { |
| | | Log.d(TAG, "No H264 files found in directory"); |
| | | return resourceList; |
| | | } |
| | | |
| | | // 解析时间范围 |
| | | Date startDate = parseTime(startTime); |
| | | Date endDate = parseTime(endTime); |
| | | |
| | | if (startDate == null || endDate == null) { |
| | | Log.e(TAG, "Invalid time format, startTime: " + startTime + ", endTime: " + endTime); |
| | | return resourceList; |
| | | } |
| | | |
| | | // 遍历文件,查找在时间范围内的文件 |
| | | for (File file : files) { |
| | | ResourceInfo resourceInfo = createResourceInfoFromFile(file, startDate, endDate); |
| | | if (resourceInfo != null) { |
| | | resourceList.add(resourceInfo); |
| | | } |
| | | } |
| | | |
| | | Log.d(TAG, "Found " + resourceList.size() + " resources in time range"); |
| | | return resourceList; |
| | | |
| | | } catch (Exception e) { |
| | | Log.e(TAG, "Error getting resource list", e); |
| | | return resourceList; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从文件创建资源信息(如果文件在时间范围内) |
| | | */ |
| | | private ResourceInfo createResourceInfoFromFile(File file, Date startDate, Date endDate) { |
| | | try { |
| | | // 从文件名或文件修改时间获取文件时间 |
| | | // 这里假设文件名包含时间戳,或者使用文件修改时间 |
| | | Date fileDate = new Date(file.lastModified()); |
| | | |
| | | // 检查文件时间是否在指定范围内 |
| | | if (fileDate.before(startDate) || fileDate.after(endDate)) { |
| | | return null; // 不在时间范围内 |
| | | } |
| | | |
| | | // 创建资源信息对象 |
| | | ResourceInfo resourceInfo = new ResourceInfo(); |
| | | |
| | | // 逻辑通道号(默认值,实际应从配置获取) |
| | | resourceInfo.setLogicalChannelNumber((byte) 1); |
| | | |
| | | // 开始时间和结束时间(使用文件修改时间) |
| | | SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss", Locale.US); |
| | | resourceInfo.setStartTime(bcdFormat.format(fileDate)); |
| | | // 结束时间可以使用文件修改时间加上一个默认时长(例如1分钟) |
| | | long fileDuration = 60000; // 默认1分钟,实际应该根据文件内容计算 |
| | | Date endTime = new Date(fileDate.getTime() + fileDuration); |
| | | resourceInfo.setEndTime(bcdFormat.format(endTime)); |
| | | |
| | | // 报警标志(默认值,实际应从文件元数据获取) |
| | | 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) { |
| | | Log.e(TAG, "Error creating resource info from file: " + file.getName(), e); |
| | | 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.US); |
| | | return format.parse(timeStr); |
| | | } catch (ParseException e) { |
| | | Log.e(TAG, "Failed to parse time: " + timeStr, e); |
| | | return null; |
| | | } |
| | | } |
| | | } |
| | | |