5个文件已修改
2个文件已添加
1433 ■■■■■ 已修改文件
app/src/main/AndroidManifest.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/FileLoggingTree.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService.java 243 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService2.java 712 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264Encoder.java 172 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/MainActivity.kt 125 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
多进程方案使用说明.md 117 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml
@@ -37,7 +37,7 @@
            </intent-filter>
        </activity>
        
        <!-- H264编码服务 -->
        <!-- H264编码服务(第一个摄像头,主进程) -->
        <service
            android:name=".H264EncodeService"
            android:enabled="true"
@@ -46,6 +46,17 @@
                <action android:name="com.anyun.h264.H264EncodeService" />
            </intent-filter>
        </service>
        <!-- H264编码服务2(第二个摄像头,独立进程) -->
        <service
            android:name=".H264EncodeService2"
            android:enabled="true"
            android:exported="true"
            android:process=":camera2">
            <intent-filter>
                <action android:name="com.anyun.h264.H264EncodeService2" />
            </intent-filter>
        </service>
    </application>
</manifest>
app/src/main/java/com/anyun/h264/FileLoggingTree.java
@@ -5,11 +5,13 @@
import timber.log.Timber;
import java.io.File;
import java.io.FileFilter;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
/**
 * Timber Tree implementation that logs to files.
@@ -19,6 +21,8 @@
    private static final String LOG_DIR = "nvlog";
    private static final String LOG_PREFIX = "h264_";
    private static final String LOG_SUFFIX = ".log";
    // æ—¥å¿—文件保留天数
    private static final int LOG_RETENTION_DAYS = 3;
    private static final String DATE_FORMAT = "yyyyMMdd";
    private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
    
@@ -36,6 +40,8 @@
            // Check if we need to update the log file (new day)
            if (!logFileName.equals(currentLogFile)) {
                currentLogFile = logFileName;
                // æ–°çš„一天,顺便清理过期日志文件
                cleanupExpiredLogFiles(LOG_RETENTION_DAYS);
            }
            
            File logFile = getLogFile(logFileName);
@@ -88,6 +94,50 @@
        } catch (IOException e) {
            Log.e("FileLoggingTree", "Error getting log file", e);
            return null;
        }
    }
    /**
     * æ¸…理超出保留天数的日志文件
     */
    private void cleanupExpiredLogFiles(int retentionDays) {
        if (retentionDays <= 0) {
            return;
        }
        try {
            File logDir = new File(Environment.getExternalStorageDirectory(), LOG_DIR);
            if (!logDir.exists() || !logDir.isDirectory()) {
                return;
            }
            long retentionMillis = TimeUnit.DAYS.toMillis(Math.max(1, retentionDays));
            long cutoffTime = System.currentTimeMillis() - retentionMillis;
            File[] files = logDir.listFiles(new FileFilter() {
                @Override
                public boolean accept(File pathname) {
                    String name = pathname.getName();
                    return name.startsWith(LOG_PREFIX) && name.endsWith(LOG_SUFFIX);
                }
            });
            if (files == null || files.length == 0) {
                return;
            }
            for (File file : files) {
                if (file.lastModified() < cutoffTime) {
                    boolean deleted = file.delete();
                    if (deleted) {
                        Log.i("FileLoggingTree", "Deleted expired log file: " + file.getAbsolutePath());
                    } else {
                        Log.w("FileLoggingTree", "Failed to delete expired log file: " + file.getAbsolutePath());
                    }
                }
            }
        } catch (Exception e) {
            Log.e("FileLoggingTree", "Error cleaning up expired log files", e);
        }
    }
    
@@ -144,3 +194,4 @@
    }
}
app/src/main/java/com/anyun/h264/H264EncodeService.java
@@ -1,7 +1,10 @@
package com.anyun.h264;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.RemoteException;
import timber.log.Timber;
@@ -19,6 +22,8 @@
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
 * H264编码服务
@@ -31,6 +36,12 @@
    private H264FileTransmitter h264FileTransmitter; // H264文件传输器
    private String outputFileDirectory; // H264文件输出目录
    private WatermarkInfo currentWatermarkInfo; // å½“前水印信息
    private static final int H264_FILE_RETENTION_DAYS = 5; // å¯æ ¹æ®éœ€æ±‚调整为3或5天
    // å¤šè¿›ç¨‹æ”¯æŒï¼šç¬¬äºŒä¸ªæ‘„像头的服务连接
    private IH264EncodeService camera2Service;
    private ServiceConnection camera2Connection;
    private boolean isCamera2Bound = false;
    
    // é»˜è®¤ç¼–码参数
    private static final int DEFAULT_WIDTH = 640;
@@ -68,6 +79,9 @@
        // åˆå§‹åŒ–输出文件目录(使用应用外部存储目录)
        outputFileDirectory = getExternalFilesDir(null).getAbsolutePath();
        Timber.d("Output file directory: %s", outputFileDirectory);
        // æ¸…理过期的H264文件
        cleanupExpiredH264Files(H264_FILE_RETENTION_DAYS);
    }
    
    @Override
@@ -91,6 +105,18 @@
        // åœæ­¢å¹¶é‡Šæ”¾ç¼–码器和文件传输器
        stopEncoder();
        stopFileTransmitter();
        // è§£ç»‘第二个进程的服务
        if (isCamera2Bound && camera2Connection != null) {
            try {
                unbindService(camera2Connection);
            } catch (Exception e) {
                Timber.e(e, "Error unbinding camera2 service");
            }
            isCamera2Bound = false;
            camera2Service = null;
            camera2Connection = null;
        }
    }
    
    /**
@@ -103,6 +129,7 @@
        int height;
        int framerate;
        String simPhone;
        Integer cameraId; // æ‘„像头ID(1或2,用于多进程方案)
        
        // ä»ŽJSON解析配置
        static EncodeConfig fromJson(String jsonConfig) throws JSONException {
@@ -115,6 +142,7 @@
                config.ip = null;
                config.port = 0;
                config.simPhone = null;
                config.cameraId = 1; // é»˜è®¤ä½¿ç”¨ç¬¬ä¸€ä¸ªæ‘„像头
                return config;
            }
            
@@ -125,6 +153,13 @@
            config.ip = json.optString("ip", null);
            config.port = json.optInt("port", 0);
            config.simPhone = json.optString("simPhone", null);
            // è§£æžcameraId(如果未指定,默认为1)
            if (json.has("cameraId")) {
                config.cameraId = json.optInt("cameraId", 1);
            } else {
                config.cameraId = 1; // é»˜è®¤ä½¿ç”¨ç¬¬ä¸€ä¸ªæ‘„像头
            }
            
            return config;
        }
@@ -171,15 +206,34 @@
     *               4-开始传输H264文件(从文件读取并网络推送),
     *               5-停止H264文件传输
     * @param jsonConfig JSON格式的配置参数
     *                   action 0/2: åŒ…含:ip、port、width、height、framerate、simPhone
     *                   action 0/2: åŒ…含:ip、port、width、height、framerate、simPhone、cameraId(可选,1或2,默认1)
     *                   action 4: åŒ…含:ip、port、framerate、simPhone、filePath、protocolType(可选,1-UDP,2-TCP,默认TCP)
     *                   action 1/3/5: æ­¤å‚数可为空或null
     *                   action 1/3/5: æ­¤å‚数可为空或null,或包含cameraId来指定要停止的摄像头
     * @return 0-成功,1-失败
     */
    private synchronized int controlEncode(int action, String jsonConfig) {
        Timber.d("controlEncode called with action: %d, jsonConfig: %s", action, jsonConfig);
        
        try {
            // è§£æžcameraId(如果配置中有)
            Integer cameraId = null;
            if (jsonConfig != null && !jsonConfig.trim().isEmpty()) {
                try {
                    JSONObject json = new JSONObject(jsonConfig);
                    if (json.has("cameraId")) {
                        cameraId = json.optInt("cameraId", 1);
                    }
                } catch (JSONException e) {
                    // å¿½ç•¥è§£æžé”™è¯¯ï¼Œç»§ç»­ä½¿ç”¨å½“前进程
                }
            }
            // å¦‚果指定了cameraId=2,路由到第二个进程
            if (cameraId != null && cameraId == 2) {
                return controlEncodeInProcess2(action, jsonConfig);
            }
            // å¦åˆ™åœ¨å½“前进程(cameraId=1)处理
            switch (action) {
                case 0: // å¼€å¯h264文件写入
                    try {
@@ -191,6 +245,10 @@
                    }
                    
                case 1: // åœæ­¢h264编码并停止写入文件
                    // æ£€æŸ¥æ˜¯å¦æŒ‡å®šäº†cameraId=2
                    if (cameraId != null && cameraId == 2) {
                        return controlEncodeInProcess2(action, jsonConfig);
                    }
                    return stopEncoder();
                    
                case 2: // å¼€å¯ç½‘络推送h264(不写入文件)
@@ -203,6 +261,10 @@
                    }
                    
                case 3: // åœæ­¢h264编码并停止网络推送
                    // æ£€æŸ¥æ˜¯å¦æŒ‡å®šäº†cameraId=2
                    if (cameraId != null && cameraId == 2) {
                        return controlEncodeInProcess2(action, jsonConfig);
                    }
                    return stopEncoder();
                    
                case 4: // å¼€å§‹ä¼ è¾“H264文件
@@ -216,6 +278,10 @@
                    
                case 5: // åœæ­¢H264文件传输
                    Timber.i("客户端请求停止视频文件上传");
                    // æ£€æŸ¥æ˜¯å¦æŒ‡å®šäº†cameraId=2
                    if (cameraId != null && cameraId == 2) {
                        return controlEncodeInProcess2(action, jsonConfig);
                    }
                    return stopFileTransmitter();
                    
                default:
@@ -226,6 +292,85 @@
            Timber.e(e, "Error in controlEncode");
            return 1; // å¤±è´¥
        }
    }
    /**
     * åœ¨ç¬¬äºŒä¸ªè¿›ç¨‹ï¼ˆcamera2)中执行编码控制
     */
    private int controlEncodeInProcess2(int action, String jsonConfig) {
        Timber.d("Routing to process 2 (camera2) for action: %d", action);
        try {
            // ç¡®ä¿ç¬¬äºŒä¸ªè¿›ç¨‹çš„æœåŠ¡å·²ç»‘å®š
            if (!ensureCamera2ServiceBound()) {
                Timber.e("Failed to bind camera2 service");
                return 1;
            }
            // è°ƒç”¨ç¬¬äºŒä¸ªè¿›ç¨‹çš„æœåŠ¡
            if (camera2Service != null) {
                return camera2Service.controlEncode(action, jsonConfig);
            } else {
                Timber.e("Camera2 service is null");
                return 1;
            }
        } catch (RemoteException e) {
            Timber.e(e, "Error calling camera2 service");
            return 1;
        }
    }
    /**
     * ç¡®ä¿ç¬¬äºŒä¸ªè¿›ç¨‹çš„æœåŠ¡å·²ç»‘å®š
     */
    private synchronized boolean ensureCamera2ServiceBound() {
        if (isCamera2Bound && camera2Service != null) {
            return true;
        }
        Timber.d("Binding to camera2 service...");
        final CountDownLatch latch = new CountDownLatch(1);
        final boolean[] success = {false};
        camera2Connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Timber.d("Camera2 service connected");
                camera2Service = IH264EncodeService.Stub.asInterface(service);
                isCamera2Bound = true;
                success[0] = true;
                latch.countDown();
            }
            @Override
            public void onServiceDisconnected(ComponentName name) {
                Timber.w("Camera2 service disconnected");
                camera2Service = null;
                isCamera2Bound = false;
            }
        };
        Intent intent = new Intent(this, H264EncodeService2.class);
        boolean bound = bindService(intent, camera2Connection, Context.BIND_AUTO_CREATE);
        if (!bound) {
            Timber.e("Failed to bind camera2 service");
            return false;
        }
        // ç­‰å¾…服务连接(最多5秒)
        try {
            if (!latch.await(5, TimeUnit.SECONDS)) {
                Timber.e("Timeout waiting for camera2 service connection");
                return false;
            }
        } catch (InterruptedException e) {
            Timber.e(e, "Interrupted while waiting for camera2 service");
            return false;
        }
        return success[0];
    }
    
    /**
@@ -245,35 +390,37 @@
            h264Encoder = new H264Encoder();
            
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null ? config.width : DEFAULT_WIDTH;
            int height = config != null ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null ? config.framerate : DEFAULT_FRAME_RATE;
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
            int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            long timeFile = System.currentTimeMillis()/1000*1000;//Date是秒,所以为了跟下发的Date starttime一致,此处除以1000 ç§’
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss");
            String str = bcdFormat.format(timeFile);
            Timber.i("文件名:%s", str);
            // è®¾ç½®è¾“出文件
            String fileName = "h264_" + timeFile+ ".h264";
            File outputFile = new File(outputFileDirectory, fileName);
            h264Encoder.setOutputFile(outputFile.getAbsolutePath());
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            
            // ç¦ç”¨ç½‘络传输
            h264Encoder.setEnableNetworkTransmission(false);
            
            // åˆå§‹åŒ–并启动(使用配置中的分辨率)
            // æ ¹æ®cameraId选择摄像头范围
            int[] cameraIdRange = DEFAULT_CAMERA_ID_RANGE;
            if (config != null && config.cameraId != null) {
                // å¦‚果指定了cameraId,使用对应的摄像头
                cameraIdRange = new int[]{config.cameraId, config.cameraId};
            }
            int[] resolution = {width, height};
            if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) {
            if (h264Encoder.initialize(cameraIdRange, null, resolution, false)) {
                // åº”用已保存的水印信息(如果有)
                if (currentWatermarkInfo != null) {
                    h264Encoder.setWatermarkInfo(currentWatermarkInfo);
                    Timber.d("Applied saved watermark info to encoder");
                }
                h264Encoder.start();
                Timber.d("File encode started successfully, output file: %s, resolution: %dx%d, framerate: %d",
                        outputFile.getAbsolutePath(), width, height, framerate);
                Timber.d("File encode started successfully, output directory: %s, resolution: %dx%d, framerate: %d",
                        outputFileDirectory, width, height, framerate);
                return 0; // æˆåŠŸ
            } else {
                Timber.e("Failed to initialize encoder");
@@ -310,22 +457,15 @@
            h264Encoder = new H264Encoder();
            
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width =  DEFAULT_WIDTH;
            int height =  DEFAULT_HEIGHT;
            int framerate = DEFAULT_FRAME_RATE;
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
            int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            long timeFile = System.currentTimeMillis()/1000*1000;
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss");
            String str = bcdFormat.format(timeFile);
            Timber.i("startNetworkEncode æ–‡ä»¶åï¼š%s", str);
            // è®¾ç½®è¾“出文件
            String fileName = "h264_" + timeFile+ ".h264";
            File outputFile = new File(outputFileDirectory, fileName);
            h264Encoder.setOutputFile(outputFile.getAbsolutePath());
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            
@@ -339,8 +479,14 @@
            h264Encoder.setProtocolParams(simPhone, (byte)1);
            
            // åˆå§‹åŒ–并启动(使用配置中的分辨率)
            // æ ¹æ®cameraId选择摄像头范围
            int[] cameraIdRange = DEFAULT_CAMERA_ID_RANGE;
            if (config != null && config.cameraId != null) {
                // å¦‚果指定了cameraId,使用对应的摄像头
                cameraIdRange = new int[]{config.cameraId, config.cameraId};
            }
            int[] resolution = {width, height};
            if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) {
            if (h264Encoder.initialize(cameraIdRange, null, resolution, false)) {
                // åº”用已保存的水印信息(如果有)
                if (currentWatermarkInfo != null) {
                    h264Encoder.setWatermarkInfo(currentWatermarkInfo);
@@ -651,6 +797,41 @@
            currentWatermarkInfo = null;
        }
    }
    /**
     * åˆ é™¤è¶…过保留期的H264文件
     */
    private void cleanupExpiredH264Files(int retentionDays) {
        if (outputFileDirectory == null) {
            Timber.w("cleanupExpiredH264Files: outputFileDirectory is null");
            return;
        }
        File dir = new File(outputFileDirectory);
        if (!dir.exists() || !dir.isDirectory()) {
            Timber.w("cleanupExpiredH264Files: directory invalid -> %s", outputFileDirectory);
            return;
        }
        long retentionMillis = TimeUnit.DAYS.toMillis(Math.max(1, retentionDays));
        long cutoffTime = System.currentTimeMillis() - retentionMillis;
        File[] files = dir.listFiles((d, name) -> name.toLowerCase(Locale.CHINA).endsWith(".h264"));
        if (files == null || files.length == 0) {
            return;
        }
        for (File file : files) {
            if (file.lastModified() < cutoffTime) {
                boolean deleted = file.delete();
                if (deleted) {
                    Timber.i("Deleted expired H264 file: %s", file.getAbsolutePath());
                } else {
                    Timber.w("Failed to delete expired H264 file: %s", file.getAbsolutePath());
                }
            }
        }
    }
    
    /**
     * ä»Žæ–‡ä»¶åˆ›å»ºèµ„源信息(如果文件在时间范围内)
app/src/main/java/com/anyun/h264/H264EncodeService2.java
New file
@@ -0,0 +1,712 @@
package com.anyun.h264;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import timber.log.Timber;
import com.anyun.h264.model.ResourceInfo;
import com.anyun.h264.model.WatermarkInfo;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
/**
 * H264编码服务2(第二个摄像头,运行在独立进程)
 * æä¾›AIDL接口供客户端调用,用于控制第二个USB摄像头的H264编码
 */
public class H264EncodeService2 extends Service {
    private static final String TAG = "H264EncodeService2";
    private H264Encoder h264Encoder;
    private H264FileTransmitter h264FileTransmitter; // H264文件传输器
    private String outputFileDirectory; // H264文件输出目录
    private WatermarkInfo currentWatermarkInfo; // å½“前水印信息
    // é»˜è®¤ç¼–码参数
    private static final int DEFAULT_WIDTH = 640;
    private static final int DEFAULT_HEIGHT = 480;
    private static final int DEFAULT_FRAME_RATE = 25;
    private static final int DEFAULT_BITRATE = 2000000; // 2Mbps
    // ç¬¬äºŒä¸ªæ‘„像头固定使用cameraId=2
    private static final int[] CAMERA2_ID_RANGE = {2, 3};
    // AIDL接口实现
    private final IH264EncodeService.Stub binder = new IH264EncodeService.Stub() {
        @Override
        public int controlEncode(int action, String jsonConfig) throws RemoteException {
            return H264EncodeService2.this.controlEncode(action, jsonConfig);
        }
        @Override
        public List<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException {
            return H264EncodeService2.this.getResourceList(startTime, endTime);
        }
        @Override
        public void setWatermarkInfo(String watermarkInfo) throws RemoteException {
            H264EncodeService2.this.setWatermarkInfo(watermarkInfo);
        }
    };
    @Override
    public void onCreate() {
        super.onCreate();
        Timber.d("H264EncodeService2 created (process 2 for camera 2)");
        // åˆå§‹åŒ–输出文件目录(使用应用外部存储目录)
        outputFileDirectory = getExternalFilesDir(null).getAbsolutePath();
        Timber.d("Output file directory: %s", outputFileDirectory);
    }
    @Override
    public IBinder onBind(Intent intent) {
        Timber.d("Service2 bound");
        return binder;
    }
    @Override
    public boolean onUnbind(Intent intent) {
        Timber.d("Service2 unbound");
        // ä¸è‡ªåŠ¨åœæ­¢ç¼–ç å™¨ï¼Œè®©å®ƒåœ¨æœåŠ¡ä¸­ä¿æŒè¿è¡Œ
        return super.onUnbind(intent);
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        Timber.d("Service2 destroyed");
        // åœæ­¢å¹¶é‡Šæ”¾ç¼–码器和文件传输器
        stopEncoder();
        stopFileTransmitter();
    }
    /**
     * ç¼–码配置类
     */
    private static class EncodeConfig {
        String ip;
        int port;
        int width;
        int height;
        int framerate;
        String simPhone;
        // ä»ŽJSON解析配置
        static EncodeConfig fromJson(String jsonConfig) throws JSONException {
            EncodeConfig config = new EncodeConfig();
            if (jsonConfig == null || jsonConfig.trim().isEmpty()) {
                // ä½¿ç”¨é»˜è®¤å€¼
                config.width = DEFAULT_WIDTH;
                config.height = DEFAULT_HEIGHT;
                config.framerate = DEFAULT_FRAME_RATE;
                config.ip = null;
                config.port = 0;
                config.simPhone = null;
                return config;
            }
            JSONObject json = new JSONObject(jsonConfig);
            config.width = json.optInt("width", DEFAULT_WIDTH);
            config.height = json.optInt("height", DEFAULT_HEIGHT);
            config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE);
            config.ip = json.optString("ip", null);
            config.port = json.optInt("port", 0);
            config.simPhone = json.optString("simPhone", null);
            return config;
        }
    }
    /**
     * æ–‡ä»¶ä¼ è¾“配置类
     */
    private static class FileTransmitConfig {
        String ip;
        int port;
        int framerate;
        String simPhone;
        String filePath; // H264文件路径
        int protocolType; // åè®®ç±»åž‹ï¼š1-UDP,2-TCP
        // ä»ŽJSON解析配置
        static FileTransmitConfig fromJson(String jsonConfig) throws JSONException {
            FileTransmitConfig config = new FileTransmitConfig();
            if (jsonConfig == null || jsonConfig.trim().isEmpty()) {
                throw new JSONException("File transmit config cannot be empty");
            }
            JSONObject json = new JSONObject(jsonConfig);
            config.ip = json.optString("ip", null);
            config.port = json.optInt("port", 0);
            config.framerate = json.optInt("framerate", DEFAULT_FRAME_RATE);
            config.simPhone = json.optString("simPhone", "013120122580");
            config.filePath = json.optString("filePath", null);
            // åè®®ç±»åž‹ï¼šé»˜è®¤TCP(2),1-UDP,2-TCP
            config.protocolType = json.optInt("protocolType", JT1076ProtocolHelper.PROTOCOL_TYPE_TCP);
            return config;
        }
    }
    /**
     * æŽ§åˆ¶H264编码和文件传输(第二个摄像头)
     */
    private synchronized int controlEncode(int action, String jsonConfig) {
        Timber.d("controlEncode (camera2) called with action: %d, jsonConfig: %s", action, jsonConfig);
        try {
            switch (action) {
                case 0: // å¼€å¯h264文件写入
                    try {
                        EncodeConfig config0 = EncodeConfig.fromJson(jsonConfig);
                        return startFileEncode(config0);
                    } catch (JSONException e) {
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
                    }
                case 1: // åœæ­¢h264编码并停止写入文件
                    return stopEncoder();
                case 2: // å¼€å¯ç½‘络推送h264(不写入文件)
                    try {
                        EncodeConfig config2 = EncodeConfig.fromJson(jsonConfig);
                        return startNetworkEncode(config2);
                    } catch (JSONException e) {
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
                    }
                case 3: // åœæ­¢h264编码并停止网络推送
                    return stopEncoder();
                case 4: // å¼€å§‹ä¼ è¾“H264文件
                    try {
                        FileTransmitConfig config4 = FileTransmitConfig.fromJson(jsonConfig);
                        return startFileTransmit(config4);
                    } catch (JSONException e) {
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
                    }
                case 5: // åœæ­¢H264文件传输
                    Timber.i("客户端请求停止视频文件上传 (camera2)");
                    return stopFileTransmitter();
                default:
                    Timber.e("Unknown action: %d", action);
                    return 1; // å¤±è´¥
            }
        } catch (Exception e) {
            Timber.e(e, "Error in controlEncode (camera2)");
            return 1; // å¤±è´¥
        }
    }
    /**
     * å¯åŠ¨æ–‡ä»¶ç¼–ç æ¨¡å¼ï¼ˆåªå†™å…¥æ–‡ä»¶ï¼Œä¸è¿›è¡Œç½‘ç»œæŽ¨é€ï¼‰
     */
    private int startFileEncode(EncodeConfig config) {
        Timber.d("Starting file encode mode (camera2)");
        // å¦‚果编码器已经在运行,先停止
        if (h264Encoder != null) {
            Timber.w("Encoder is already running (camera2), stopping it first");
            stopEncoder();
        }
        try {
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
            int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            // ç¦ç”¨ç½‘络传输
            h264Encoder.setEnableNetworkTransmission(false);
            // åˆå§‹åŒ–并启动(使用第二个摄像头,cameraId=2)
            int[] resolution = {width, height};
            if (h264Encoder.initialize(CAMERA2_ID_RANGE, null, resolution, false)) {
                // åº”用已保存的水印信息(如果有)
                if (currentWatermarkInfo != null) {
                    h264Encoder.setWatermarkInfo(currentWatermarkInfo);
                    Timber.d("Applied saved watermark info to encoder (camera2)");
                }
                h264Encoder.start();
                Timber.d("File encode started successfully (camera2), output directory: %s, resolution: %dx%d, framerate: %d",
                        outputFileDirectory, width, height, framerate);
                return 0; // æˆåŠŸ
            } else {
                Timber.e("Failed to initialize encoder (camera2)");
                h264Encoder = null;
                return 1; // å¤±è´¥
            }
        } catch (Exception e) {
            Timber.e(e, "Failed to start file encode (camera2)");
            h264Encoder = null;
            return 1; // å¤±è´¥
        }
    }
    /**
     * å¯åŠ¨ç½‘ç»œæŽ¨é€æ¨¡å¼ï¼ˆåªè¿›è¡Œç½‘ç»œæŽ¨é€ï¼Œä¸å†™å…¥æ–‡ä»¶ï¼‰
     */
    private int startNetworkEncode(EncodeConfig config) {
        Timber.d("Starting network encode mode (camera2)");
        // å¦‚果编码器已经在运行,先停止
        if (h264Encoder != null) {
            Timber.w("Encoder is already running (camera2), stopping it first");
            stopEncoder();
        }
        // æ£€æŸ¥å¿…需的配置参数
        if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
            Timber.e("Network encode requires valid ip and port in config (camera2)");
            return 1; // å¤±è´¥
        }
        try {
            // åˆ›å»ºç¼–码器
            h264Encoder = new H264Encoder();
            // è®¾ç½®ç¼–码参数(使用配置中的参数)
            int width = config != null && config.width > 0 ? config.width : DEFAULT_WIDTH;
            int height = config != null && config.height > 0 ? config.height : DEFAULT_HEIGHT;
            int framerate = config != null && config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            // è®¾ç½®è¾“出文件目录(H264Encoder会自动管理文件创建,每分钟一个文件)
            // ä½¿ç”¨ä¸€ä¸ªä¸´æ—¶æ–‡ä»¶åæ¥è®¾ç½®ç›®å½•,H264Encoder会在初始化时创建第一个文件
            File tempFile = new File(outputFileDirectory, "temp.h264");
            h264Encoder.setOutputFile(tempFile.getAbsolutePath());
            h264Encoder.setEnableFileOutput(true); // å¯ç”¨æ–‡ä»¶è¾“出
            // å¯ç”¨ç½‘络传输并设置服务器地址
            h264Encoder.setEnableNetworkTransmission(true);
            h264Encoder.setServerAddress(config.ip, config.port);
            // è®¾ç½®åè®®å‚数(使用配置中的simPhone,如果未提供则使用默认值)
            String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty()
                    ? config.simPhone : "013120122580";
            h264Encoder.setProtocolParams(simPhone, (byte)2); // ç¬¬äºŒä¸ªæ‘„像头使用channelId=2
            // åˆå§‹åŒ–并启动(使用第二个摄像头,cameraId=2)
            int[] resolution = {width, height};
            if (h264Encoder.initialize(CAMERA2_ID_RANGE, null, resolution, false)) {
                // åº”用已保存的水印信息(如果有)
                if (currentWatermarkInfo != null) {
                    h264Encoder.setWatermarkInfo(currentWatermarkInfo);
                    Timber.d("Applied saved watermark info to encoder (camera2)");
                }
                h264Encoder.start();
                Timber.d("Network encode started successfully (camera2), server: %s:%d, resolution: %dx%d, framerate: %d",
                        config.ip, config.port, width, height, framerate);
                return 0; // æˆåŠŸ
            } else {
                Timber.e("Failed to initialize encoder (camera2)");
                h264Encoder = null;
                return 1; // å¤±è´¥
            }
        } catch (Exception e) {
            Timber.e(e, "Failed to start network encode (camera2)");
            h264Encoder = null;
            return 1; // å¤±è´¥
        }
    }
    /**
     * åœæ­¢ç¼–码器
     */
    private int stopEncoder() {
        Timber.d("Stopping encoder (camera2)");
        if (h264Encoder != null) {
            try {
                h264Encoder.stop();
                h264Encoder.release();
                h264Encoder = null;
                Timber.d("Encoder stopped successfully (camera2)");
                return 0; // æˆåŠŸ
            } catch (Exception e) {
                Timber.e(e, "Error stopping encoder (camera2)");
                h264Encoder = null;
                return 1; // å¤±è´¥
            }
        } else {
            Timber.w("Encoder is not running (camera2)");
            return 0; // æˆåŠŸï¼ˆæ²¡æœ‰è¿è¡Œçš„ç¼–ç å™¨ï¼Œè§†ä¸ºæˆåŠŸï¼‰
        }
    }
    /**
     * å¯åŠ¨æ–‡ä»¶ä¼ è¾“æ¨¡å¼ï¼ˆä»ŽH264文件读取并网络推送)
     */
    private int startFileTransmit(FileTransmitConfig config) {
        Timber.d("Starting file transmit mode (camera2)");
        // å¦‚果文件传输器已经在运行,先停止
        if (h264FileTransmitter != null) {
            Timber.w("File transmitter is already running (camera2), stopping it first");
            stopFileTransmitter();
        }
        // æ£€æŸ¥å¿…需的配置参数
        if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
            Timber.e("File transmit requires valid ip and port in config (camera2)");
            return 1; // å¤±è´¥
        }
        if (config.filePath == null || config.filePath.trim().isEmpty()) {
            Timber.e("File transmit requires valid filePath in config (camera2)");
            return 1; // å¤±è´¥
        }
        try {
            // æ£€æŸ¥æ–‡ä»¶æ˜¯å¦å­˜åœ¨
            File file = new File(config.filePath);
            if (!file.exists() || !file.isFile()) {
                Timber.e("File does not exist: %s (camera2)", config.filePath);
                return 1; // å¤±è´¥
            }
            // åˆ›å»ºæ–‡ä»¶ä¼ è¾“器
            h264FileTransmitter = new H264FileTransmitter();
            // è®¾ç½®æœåŠ¡å™¨åœ°å€
            h264FileTransmitter.setServerAddress(config.ip, config.port);
            // è®¾ç½®åè®®ç±»åž‹
            h264FileTransmitter.setProtocolType(1); //1-tcp
            // è®¾ç½®åè®®å‚数(SIM卡号和逻辑通道号,第二个摄像头使用channelId=2)
            String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty()
                    ? config.simPhone : "013120122580";
            h264FileTransmitter.setProtocolParams(simPhone, (byte)2);
            // è®¾ç½®å¸§çŽ‡ï¼ˆç”¨äºŽè®¡ç®—æ—¶é—´æˆ³é—´éš”ï¼‰
            int framerate = config.framerate > 0 ? config.framerate : DEFAULT_FRAME_RATE;
            h264FileTransmitter.setFrameRate(framerate);
            // è®¾ç½®è¿›åº¦å›žè°ƒï¼ˆå¯é€‰ï¼Œç”¨äºŽæ—¥å¿—输出)
            h264FileTransmitter.setOnTransmitProgressCallback(new H264FileTransmitter.OnTransmitProgressCallback() {
                @Override
                public void onProgress(int currentFrame, int totalFrames) {
                    Timber.d("File transmit progress (camera2): frame %d%s", currentFrame,
                            totalFrames > 0 ? " of " + totalFrames : "");
                }
                @Override
                public void onComplete() {
                    Timber.d("File transmit completed (camera2)");
                    stopFileTransmitter();
                }
                @Override
                public void onError(String error) {
                    Timber.e("File transmit error (camera2): %s", error);
                }
            });
            // åˆå§‹åŒ–Socket连接
            if (!h264FileTransmitter.initialize()) {
                Timber.e("Failed to initialize file transmitter socket (camera2)");
                h264FileTransmitter = null;
                return 1; // å¤±è´¥
            }
            // å¼€å§‹ä¼ è¾“文件
            h264FileTransmitter.transmitFile(config.filePath);
            Timber.d("File transmit started successfully (camera2), file: %s, server: %s:%d, protocol: %s, framerate: %d",
                    config.filePath, config.ip, config.port,
                    config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP", framerate);
            return 0; // æˆåŠŸ
        } catch (Exception e) {
            Timber.e(e, "Failed to start file transmit (camera2)");
            if (h264FileTransmitter != null) {
                try {
                    h264FileTransmitter.stop();
                } catch (Exception ex) {
                    Timber.e(ex, "Error stopping file transmitter after failure (camera2)");
                }
                h264FileTransmitter = null;
            }
            return 1; // å¤±è´¥
        }
    }
    /**
     * åœæ­¢æ–‡ä»¶ä¼ è¾“器
     */
    private int stopFileTransmitter() {
        Timber.d("Stopping file transmitter (camera2)");
        if (h264FileTransmitter != null) {
            try {
                h264FileTransmitter.stop();
                h264FileTransmitter = null;
                Timber.d("File transmitter stopped successfully (camera2)");
                return 0; // æˆåŠŸ
            } catch (Exception e) {
                Timber.e(e, "Error stopping file transmitter (camera2)");
                h264FileTransmitter = null;
                return 1; // å¤±è´¥
            }
        } else {
            Timber.w("File transmitter is not running (camera2)");
            return 0; // æˆåŠŸï¼ˆæ²¡æœ‰è¿è¡Œçš„æ–‡ä»¶ä¼ è¾“å™¨ï¼Œè§†ä¸ºæˆåŠŸï¼‰
        }
    }
    /**
     * èŽ·å–èµ„æºåˆ—è¡¨ï¼ˆæ ¹æ®JT/T 1076-2016表23定义)
     */
    private List<ResourceInfo> getResourceList(String startTime, String endTime) {
        Timber.d("getResourceList called (camera2), startTime: %s, endTime: %s", startTime, endTime);
        List<ResourceInfo> resourceList = new ArrayList<>();
        try {
            // æ‰«æè¾“出目录中的H264文件(只查找camera2的文件)
            File dir = new File(outputFileDirectory);
            if (!dir.exists() || !dir.isDirectory()) {
                Timber.w("Output directory does not exist: %s", outputFileDirectory);
                return resourceList;
            }
            File[] files = dir.listFiles((dir1, name) ->
                name.toLowerCase().endsWith(".h264") && name.contains("camera2"));
            if (files == null || files.length == 0) {
                Timber.d("No H264 files found for camera2 in directory");
                return resourceList;
            }
            // è§£æžæ—¶é—´èŒƒå›´
            Date startDate = parseTime(startTime);
            Date endDate = parseTime(endTime);
            if (startDate == null || endDate == null) {
                Timber.e("Invalid time format, startTime: %s, endTime: %s", startTime, endTime);
                return resourceList;
            }
            // éåŽ†æ–‡ä»¶ï¼ŒæŸ¥æ‰¾åœ¨æ—¶é—´èŒƒå›´å†…çš„æ–‡ä»¶
            for (File file : files) {
                ResourceInfo resourceInfo = createResourceInfoFromFile(file, startDate, endDate);
                if (resourceInfo != null) {
                    resourceList.add(resourceInfo);
                }
            }
            Timber.d("Found %d resources for camera2 in time range", resourceList.size());
            return resourceList;
        } catch (Exception e) {
            Timber.e(e, "Error getting resource list (camera2)");
            return resourceList;
        }
    }
    /**
     * è®¾ç½®æ°´å°ä¿¡æ¯
     */
    private void setWatermarkInfo(String watermarkInfoJson) {
        Timber.d("setWatermarkInfo called (camera2), watermarkInfoJson: %s", watermarkInfoJson);
        try {
            if (watermarkInfoJson == null || watermarkInfoJson.trim().isEmpty()) {
                Timber.w("Watermark info JSON is null or empty, clearing watermark (camera2)");
                currentWatermarkInfo = null;
                // å¦‚果编码器正在运行,清除水印
                if (h264Encoder != null) {
                    h264Encoder.setWatermarkInfo(null);
                }
                return;
            }
            // è§£æžJSON
            JSONObject json = new JSONObject(watermarkInfoJson);
            WatermarkInfo watermarkInfo = new WatermarkInfo();
            // è§£æžå„个字段(使用 optString/optDouble é¿å…å­—段不存在时抛出异常)
            watermarkInfo.setPlateNumber(json.optString("plateNumber", null));
            watermarkInfo.setStudent(json.optString("student", null));
            watermarkInfo.setCoach(json.optString("coach", null));
            // ç»åº¦å’Œçº¬åº¦å¯èƒ½æ˜¯æ•°å­—或字符串
            if (json.has("longitude")) {
                Object lonObj = json.get("longitude");
                if (lonObj instanceof Number) {
                    watermarkInfo.setLongitude(((Number) lonObj).doubleValue());
                } else if (lonObj instanceof String) {
                    try {
                        watermarkInfo.setLongitude(Double.parseDouble((String) lonObj));
                    } catch (NumberFormatException e) {
                        Timber.w("Invalid longitude format: %s", lonObj);
                    }
                }
            }
            if (json.has("latitude")) {
                Object latObj = json.get("latitude");
                if (latObj instanceof Number) {
                    watermarkInfo.setLatitude(((Number) latObj).doubleValue());
                } else if (latObj instanceof String) {
                    try {
                        watermarkInfo.setLatitude(Double.parseDouble((String) latObj));
                    } catch (NumberFormatException e) {
                        Timber.w("Invalid latitude format: %s", latObj);
                    }
                }
            }
            watermarkInfo.setDrivingSchool(json.optString("drivingSchool", null));
            // è½¦é€Ÿå¯èƒ½æ˜¯æ•°å­—或字符串
            if (json.has("speed")) {
                Object speedObj = json.get("speed");
                if (speedObj instanceof Number) {
                    watermarkInfo.setSpeed(((Number) speedObj).doubleValue());
                } else if (speedObj instanceof String) {
                    try {
                        watermarkInfo.setSpeed(Double.parseDouble((String) speedObj));
                    } catch (NumberFormatException e) {
                        Timber.w("Invalid speed format: %s", speedObj);
                    }
                }
            }
            // ä¿å­˜æ°´å°ä¿¡æ¯
            currentWatermarkInfo = watermarkInfo;
            Timber.i("Watermark info parsed successfully (camera2): %s", watermarkInfo);
            // å¦‚果编码器正在运行,立即应用水印
            if (h264Encoder != null) {
                h264Encoder.setWatermarkInfo(watermarkInfo);
                Timber.d("Watermark applied to encoder (camera2)");
            } else {
                Timber.d("Encoder not running, watermark will be applied when encoder starts (camera2)");
            }
        } catch (JSONException e) {
            Timber.e(e, "Failed to parse watermark info JSON (camera2): %s", watermarkInfoJson);
            currentWatermarkInfo = null;
        } catch (Exception e) {
            Timber.e(e, "Unexpected error setting watermark info (camera2)");
            currentWatermarkInfo = null;
        }
    }
    /**
     * ä»Žæ–‡ä»¶åˆ›å»ºèµ„源信息(如果文件在时间范围内)
     */
    private ResourceInfo createResourceInfoFromFile(File file, Date startDate, Date endDate) {
        try {
            // ä»Žæ–‡ä»¶åä¸­æå–时间戳(格式:h264_camera2_1234567890123.h264)
            String fileName = file.getName();
            Date startTimeFromFileName = null;
            if (fileName.startsWith("h264_camera2_") && fileName.endsWith(".h264")) {
                try {
                    // æå–文件名中的时间戳
                    String timestampStr = fileName.substring(14, fileName.length() - 5); // åŽ»æŽ‰ "h264_camera2_" å’Œ ".h264"
                    long timestamp = Long.parseLong(timestampStr);
                    startTimeFromFileName = new Date(timestamp);
                } catch (NumberFormatException e) {
                    Timber.w("Failed to parse timestamp from filename: %s", fileName);
                }
            }
            // å¦‚果无法从文件名解析时间戳,则使用文件修改时间作为开始时间
            if (startTimeFromFileName == null) {
                startTimeFromFileName = new Date(file.lastModified());
            }
            // ç»“束时间使用文件修改时间
            Date endTimeFromFile = new Date(file.lastModified());
            // æ£€æŸ¥æ–‡ä»¶æ—¶é—´æ˜¯å¦åœ¨æŒ‡å®šèŒƒå›´å†…
            if (startTimeFromFileName.after(endDate) || endTimeFromFile.before(startDate)) {
                return null; // ä¸åœ¨æ—¶é—´èŒƒå›´å†…
            }
            // åˆ›å»ºèµ„源信息对象
            ResourceInfo resourceInfo = new ResourceInfo();
            // é€»è¾‘通道号(第二个摄像头使用channelId=2)
            resourceInfo.setLogicalChannelNumber((byte) 2);
            // å¼€å§‹æ—¶é—´ï¼šä»Žæ–‡ä»¶åä¸­çš„æ—¶é—´æˆ³
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            resourceInfo.setStartTime(bcdFormat.format(startTimeFromFileName));
            // ç»“束时间:使用文件修改时间
            resourceInfo.setEndTime(bcdFormat.format(endTimeFromFile));
            // æŠ¥è­¦æ ‡å¿—(默认值,实际应从文件元数据获取)
            resourceInfo.setAlarmFlag(0L);
            // éŸ³è§†é¢‘资源类型:2-视频
            resourceInfo.setResourceType((byte) 2);
            // ç æµç±»åž‹ï¼š1-主码流
            resourceInfo.setStreamType((byte) 1);
            // å­˜å‚¨å™¨ç±»åž‹ï¼š1-主存储器
            resourceInfo.setStorageType((byte) 1);
            // æ–‡ä»¶å¤§å°
            resourceInfo.setFileSize(file.length());
            return resourceInfo;
        } catch (Exception e) {
            Timber.e(e, "Error creating resource info from file: %s", file.getName());
            return null;
        }
    }
    /**
     * è§£æžBCD时间字符串
     */
    private Date parseTime(String timeStr) {
        if (timeStr == null || timeStr.length() != 12) {
            return null;
        }
        try {
            SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            return format.parse(timeStr);
        } catch (ParseException e) {
            Timber.e(e, "Failed to parse time: %s", timeStr);
            return null;
        }
    }
}
app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -76,8 +76,12 @@
    // æ–‡ä»¶è¾“出
    private FileOutputStream fileOutputStream;
    private String outputFilePath;
    private String outputFileDirectory; // è¾“出文件目录
    private boolean enableFileOutput = false; // æ˜¯å¦å¯ç”¨æ–‡ä»¶è¾“出
    private boolean spsPpsWritten = false; // æ ‡è®°SPS/PPS是否已写入
    private int cameraId = 1; // æ‘„像头ID,默认为1(第一个摄像头)
    private long currentFileStartTime = 0; // å½“前文件的开始时间(毫秒)
    private static final long FILE_DURATION_MS = 60 * 1000; // æ–‡ä»¶æ—¶é•¿ï¼š1分钟(毫秒)
    // ç½‘络传输控制
    private boolean enableNetworkTransmission = true; // æ˜¯å¦å¯ç”¨TCP/UDP网络传输
@@ -135,6 +139,22 @@
     */
    public void setOutputFile(String filePath) {
        this.outputFilePath = filePath;
        // æå–目录路径
        if (filePath != null && !filePath.isEmpty()) {
            File file = new File(filePath);
            File parentDir = file.getParentFile();
            if (parentDir != null) {
                this.outputFileDirectory = parentDir.getAbsolutePath();
            }
        }
    }
    /**
     * è®¾ç½®æ‘„像头ID(用于生成文件名)
     * @param cameraId æ‘„像头ID,1表示第一个摄像头,2表示第二个摄像头
     */
    public void setCameraId(int cameraId) {
        this.cameraId = cameraId;
    }
    /**
@@ -241,6 +261,11 @@
     */
    public boolean initialize(int[] cameraIdRange, String cameraName, int[] resolution, boolean ayCamera) {
        try {
            // ä»ŽcameraIdRange中提取cameraId(使用第一个值)
            if (cameraIdRange != null && cameraIdRange.length > 0) {
                this.cameraId = cameraIdRange[0];
            }
            // 1. setenv
            usbCamera.setenv();
@@ -277,7 +302,7 @@
            // æ›´æ–°å®žé™…分辨率
            width = actualResolution[0];
            height = actualResolution[1];
            Timber.d("Camera initialized with resolution: " + width + "x" + height);
            Timber.d("Camera initialized with resolution: " + width + "x" + height + ", cameraId: " + cameraId);
            // 3. åˆå§‹åŒ–H264编码器
            initEncoder();
@@ -293,7 +318,7 @@
            }
            // 5. åˆå§‹åŒ–文件输出(仅创建文件,SPS/PPS在第一次输出时写入)
            if (enableFileOutput && outputFilePath != null && !outputFilePath.isEmpty()) {
            if (enableFileOutput) {
                if (!initFileOutput()) {
                    Timber.w("File output initialization failed, continuing without file output");
                }
@@ -333,21 +358,35 @@
     */
    private boolean initFileOutput() {
        try {
            File file = new File(outputFilePath);
            File parentDir = file.getParentFile();
            if (parentDir != null && !parentDir.exists()) {
                boolean created = parentDir.mkdirs();
                if (!created && !parentDir.exists()) {
                    Timber.e("Failed to create parent directory: " + parentDir.getAbsolutePath());
            // å¦‚æžœoutputFileDirectory为空,尝试从outputFilePath提取
            if (outputFileDirectory == null || outputFileDirectory.isEmpty()) {
                if (outputFilePath != null && !outputFilePath.isEmpty()) {
                    File file = new File(outputFilePath);
                    File parentDir = file.getParentFile();
                    if (parentDir != null) {
                        outputFileDirectory = parentDir.getAbsolutePath();
                    }
                }
            }
            // å¦‚果仍然没有目录,使用默认路径
            if (outputFileDirectory == null || outputFileDirectory.isEmpty()) {
                Timber.e("Output file directory is not set");
                return false;
            }
            // åˆ›å»ºç›®å½•(如果不存在)
            File dir = new File(outputFileDirectory);
            if (!dir.exists()) {
                boolean created = dir.mkdirs();
                if (!created && !dir.exists()) {
                    Timber.e("Failed to create output directory: " + outputFileDirectory);
                    return false;
                }
            }
            fileOutputStream = new FileOutputStream(file);
            spsPpsWritten = false;
            Timber.d("File output initialized: " + outputFilePath);
            return true;
            // åˆ›å»ºç¬¬ä¸€ä¸ªæ–‡ä»¶
            return createNewFile();
        } catch (Exception e) {
            Timber.e(e,"Initialize file output failed");
            if (fileOutputStream != null) {
@@ -361,9 +400,67 @@
            return false;
        }
    }
    /**
     * åˆ›å»ºæ–°æ–‡ä»¶ï¼ˆæ¯åˆ†é’Ÿè°ƒç”¨ä¸€æ¬¡ï¼‰
     * @return æ˜¯å¦æˆåŠŸ
     */
    private boolean createNewFile() {
        try {
            // å…³é—­æ—§æ–‡ä»¶
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.flush();
                    fileOutputStream.close();
                    Timber.d("Closed previous file: " + outputFilePath);
                } catch (IOException e) {
                    Timber.e(e, "Error closing previous file");
                }
                fileOutputStream = null;
            }
            // ç”Ÿæˆæ–°æ–‡ä»¶å
            long timeFile = System.currentTimeMillis() / 1000 * 1000;
            currentFileStartTime = timeFile;
            String fileName;
            if (cameraId == 2) {
                fileName = "h264_camera2_" + timeFile + ".h264";
            } else {
                fileName = "h264_" + timeFile + ".h264";
            }
            File newFile = new File(outputFileDirectory, fileName);
            outputFilePath = newFile.getAbsolutePath();
            // åˆ›å»ºæ–°æ–‡ä»¶
            fileOutputStream = new FileOutputStream(newFile);
            spsPpsWritten = false; // é‡ç½®SPS/PPS标记,新文件需要重新写入
            // å¦‚果已经有缓存的SPS/PPS,立即写入新文件
            if (spsBuffer != null && ppsBuffer != null) {
                writeSpsPpsToFile();
                Timber.d("SPS/PPS written to new file immediately");
            }
            Timber.d("Created new file: " + outputFilePath);
            return true;
        } catch (Exception e) {
            Timber.e(e, "Failed to create new file");
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException ie) {
                    Timber.e(ie, "Close file output stream failed");
                }
                fileOutputStream = null;
            }
            return false;
        }
    }
    /**
     * å†™å…¥SPS/PPS到文件(从CSD或关键帧数据中提取)
     * å†™å…¥SPS/PPS到文件(优先使用缓存的SPS/PPS,否则从CSD获取)
     */
    private void writeSpsPpsToFile() {
        if (!enableFileOutput || fileOutputStream == null || spsPpsWritten) {
@@ -371,17 +468,29 @@
        }
        try {
            // å°è¯•从编码器输出格式中获取CSD
            MediaFormat format = encoder.getOutputFormat();
            ByteBuffer spsBuffer = format.getByteBuffer("csd-0"); // SPS
            ByteBuffer ppsBuffer = format.getByteBuffer("csd-1"); // PPS
            // ä¼˜å…ˆä½¿ç”¨ç¼“存的SPS/PPS(这些是Annex-B格式,已经包含起始码)
            if (spsBuffer != null && ppsBuffer != null) {
                // ç›´æŽ¥å†™å…¥ç¼“存的SPS/PPS(已经是Annex-B格式)
                fileOutputStream.write(spsBuffer);
                fileOutputStream.write(ppsBuffer);
                fileOutputStream.flush();
                spsPpsWritten = true;
                Timber.d("SPS/PPS written to file from cache, SPS size: " + spsBuffer.length + ", PPS size: " + ppsBuffer.length);
                return;
            }
            // å¦‚果缓存中没有,尝试从编码器输出格式中获取CSD
            MediaFormat format = encoder.getOutputFormat();
            ByteBuffer csdSpsBuffer = format.getByteBuffer("csd-0"); // SPS
            ByteBuffer csdPpsBuffer = format.getByteBuffer("csd-1"); // PPS
            if (csdSpsBuffer != null && csdPpsBuffer != null) {
                // CSD格式通常是AVCC格式,需要转换为Annex-B
                byte[] sps = new byte[spsBuffer.remaining()];
                byte[] pps = new byte[ppsBuffer.remaining()];
                spsBuffer.get(sps);
                ppsBuffer.get(pps);
                byte[] sps = new byte[csdSpsBuffer.remaining()];
                byte[] pps = new byte[csdPpsBuffer.remaining()];
                csdSpsBuffer.get(sps);
                csdPpsBuffer.get(pps);
                // å†™å…¥SPS和PPS到文件(Annex-B格式)
                byte[] nalStartCode = {0x00, 0x00, 0x00, 0x01};
@@ -415,9 +524,9 @@
                fileOutputStream.flush();
                spsPpsWritten = true;
                Timber.d("SPS/PPS written to file, SPS size: " + spsLength + ", PPS size: " + ppsLength);
                Timber.d("SPS/PPS written to file from CSD, SPS size: " + spsLength + ", PPS size: " + ppsLength);
            } else {
                Timber.w("SPS/PPS not found in CSD, will extract from first key frame");
                Timber.w("SPS/PPS not found in cache or CSD, will extract from first key frame");
            }
        } catch (Exception e) {
            Timber.e(e,"Write SPS/PPS to file error");
@@ -680,6 +789,17 @@
        }
        try {
            // æ£€æŸ¥æ˜¯å¦éœ€è¦åˆ›å»ºæ–°æ–‡ä»¶ï¼ˆæ¯åˆ†é’Ÿï¼‰
            long currentTime = System.currentTimeMillis();
            if (currentFileStartTime > 0 && (currentTime - currentFileStartTime) >= FILE_DURATION_MS) {
                Timber.d("File duration reached 1 minute, creating new file");
                if (!createNewFile()) {
                    Timber.e("Failed to create new file, stopping file output");
                    enableFileOutput = false;
                    return;
                }
            }
            // å¦‚果是第一个关键帧,确保SPS/PPS已写入
            if (isKeyFrame && !spsPpsWritten) {
                writeSpsPpsToFile();
app/src/main/java/com/anyun/h264/MainActivity.kt
@@ -1,6 +1,8 @@
package com.anyun.h264
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
@@ -11,6 +13,7 @@
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.anyun.h264.H264FileTransmitter.OnTransmitProgressCallback
import com.anyun.h264.ui.theme.MyApplicationTheme
import timber.log.Timber
import java.io.File
@@ -18,6 +21,9 @@
class MainActivity : ComponentActivity() {
    private var h264Encoder: H264Encoder? = null
    private var transmitter: H264FileTransmitter? = null
    private var fileList: List<File> = emptyList()
    private var currentFileIndex: Int = 0
    private val handler = Handler(Looper.getMainLooper())
    companion object{
        const val TAG ="MainActivity"
    }
@@ -34,13 +40,14 @@
                        modifier = Modifier.padding(innerPadding),
                        isRunning = isRunning,
                        onStartH264Click = {
                            val success = startH264Encoder()
                            val success = startFileTransmitter()
                            if (success) {
                                isRunning = true
                            }
                        },
                        onStopH264Click = {
                            stopH264Encoder()
//                            stopH264Encoder()
                            stopFileTransmitter()
                            isRunning = false
                        }
                    )
@@ -52,16 +59,37 @@
    override fun onDestroy() {
        super.onDestroy()
        stopH264Encoder()
        stopFileTransmitter()
    }
    private fun startFileTransmitter():Boolean {
        if (transmitter != null) {
            Timber.w("H264Encoder is already running")
            Timber.w("FileTransmitter is already running")
            return false
        }
        // èŽ·å–ç›®å½•ä¸­çš„æ‰€æœ‰ .h264 æ–‡ä»¶
        val directory = application.applicationContext.getExternalFilesDir(null)
        Timber.i("视频目录=${directory?.absolutePath}")
        if (directory?.isDirectory != true) {
            Timber.e("Directory is not valid: ${directory?.absolutePath}")
            return false
        }
        // èŽ·å–æ‰€æœ‰ .h264 æ–‡ä»¶å¹¶æŒ‰æ–‡ä»¶åæŽ’序
        fileList = directory.listFiles()
            ?.filter { it.isFile && it.name.endsWith(".h264", ignoreCase = true) }
            ?.sortedBy { it.name }
            ?: emptyList()
        if (fileList.isEmpty()) {
            Timber.w("No .h264 files found in directory")
            return false
        }
        Timber.i("Found ${fileList.size} .h264 files to transmit")
        currentFileIndex = 0
        try {
            transmitter = H264FileTransmitter()
@@ -69,28 +97,99 @@
            transmitter?.setServerAddress("192.168.16.138", 1078)
            transmitter?.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP) // æˆ– PROTOCOL_TYPE_UDP
            // è®¾ç½®åè®®å‚æ•°
            transmitter?.setProtocolParams("013120122580", 1.toByte())
            // è®¾ç½®å¸§çŽ‡ï¼ˆç”¨äºŽè®¡ç®—æ—¶é—´æˆ³é—´éš”ï¼‰
            transmitter?.setFrameRate(25)
            transmitter?.setOnTransmitProgressCallback(object : OnTransmitProgressCallback {
                override fun onProgress(currentFrame: Int, totalFrames: Int) {
                    val currentFile = if (currentFileIndex < fileList.size) fileList[currentFileIndex] else null
                    Timber.d("Transmitting file ${currentFileIndex + 1}/${fileList.size}: ${currentFile?.name}, frame: $currentFrame")
                }
                override fun onComplete() {
                    val currentFile = if (currentFileIndex < fileList.size) fileList[currentFileIndex] else null
                    Timber.i("File transmission complete: ${currentFile?.name} (${currentFileIndex + 1}/${fileList.size})")
                    // ä½¿ç”¨ Handler å»¶è¿Ÿè°ƒç”¨ï¼Œç¡®ä¿å‰ä¸€ä¸ªæ–‡ä»¶çš„传输状态已重置
                    currentFileIndex++
                    handler.postDelayed({
                        transmitNextFile()
                    }, 100) // å»¶è¿Ÿ100ms,确保前一个文件的 finally å—已执行
                }
                override fun onError(error: String?) {
                    val currentFile = if (currentFileIndex < fileList.size) fileList[currentFileIndex] else null
                    Timber.e("File transmission error: ${currentFile?.name}, error: $error")
                    // å³ä½¿å‡ºé”™ä¹Ÿç»§ç»­ä¼ è¾“下一个文件
                    // ä½¿ç”¨ Handler å»¶è¿Ÿè°ƒç”¨ï¼Œç¡®ä¿å‰ä¸€ä¸ªæ–‡ä»¶çš„传输状态已重置
                    currentFileIndex++
                    handler.postDelayed({
                        transmitNextFile()
                    }, 100) // å»¶è¿Ÿ100ms,确保前一个文件的 finally å—已执行
                }
            })
            // åˆå§‹åŒ–Socket
            if (transmitter?.initialize()==true) {
                // å¼€å§‹ä¼ è¾“文件
                transmitter?.transmitFile("/storage/emulated/0/Android/data/com.anyun.h264/files/h264_1764574451071.h264")
            if (transmitter?.initialize() == true) {
                // å¼€å§‹ä¼ è¾“第一个文件
                transmitNextFile()
                return true
            }else{
            } else {
                Timber.e("Failed to initialize transmitter")
                transmitter = null
                return false
            }
        } catch (e: Exception) {
            Timber.e(e, "Failed to start H264Encoder")
            Timber.e(e, "Failed to start FileTransmitter")
            transmitter = null
            return false
        }
    }
    /**
     * ä¼ è¾“下一个文件
     */
    private fun transmitNextFile() {
        if (transmitter == null) {
            Timber.w("Transmitter is null, cannot transmit next file")
            return
        }
        if (currentFileIndex >= fileList.size) {
            Timber.i("All files transmission complete! Total: ${fileList.size} files")
            // æ‰€æœ‰æ–‡ä»¶ä¼ è¾“完成,停止传输器
            stopFileTransmitter()
            return
        }
        val nextFile = fileList[currentFileIndex]
        Timber.i("Starting transmission of file ${currentFileIndex + 1}/${fileList.size}: ${nextFile.name}")
        // ä¼ è¾“下一个文件
        transmitter?.transmitFile(nextFile.absolutePath)
    }
    /**
     * åœæ­¢æ–‡ä»¶ä¼ è¾“器
     */
    private fun stopFileTransmitter() {
        // ç§»é™¤æ‰€æœ‰å¾…处理的 Handler ä»»åŠ¡
        handler.removeCallbacksAndMessages(null)
        transmitter?.let { tx ->
            try {
                tx.stop()
                Timber.d("FileTransmitter stopped")
            } catch (e: Exception) {
                Timber.e(e, "Failed to stop FileTransmitter")
            }
        }
        transmitter = null
        fileList = emptyList()
        currentFileIndex = 0
    }
    private fun startH264Encoder(): Boolean {
@@ -109,11 +208,11 @@
            // è®¾ç½®è¾“出文件(可选)
            val outputFile = File(getExternalFilesDir(null), "test2.h264")
            h264Encoder?.setOutputFile(outputFile.absolutePath)
            h264Encoder?.setEnableFileOutput(false) // å¯ç”¨æ–‡ä»¶è¾“出
            h264Encoder?.setEnableFileOutput(true) // å¯ç”¨æ–‡ä»¶è¾“出
            // è®¾ç½®UDP服务器地址(可选)
//             h264Encoder?.setServerAddress("58.48.93.67", 11935)
            h264Encoder?.setEnableNetworkTransmission(true)
            h264Encoder?.setEnableNetworkTransmission(false)
            h264Encoder?.setServerAddress("192.168.16.138", 1078)
            h264Encoder?.setProtocolParams("013120122580", 1)
¶à½ø³Ì·½°¸Ê¹ÓÃ˵Ã÷.md
New file
@@ -0,0 +1,117 @@
# å¤šè¿›ç¨‹æ–¹æ¡ˆä½¿ç”¨è¯´æ˜Ž
## æ¦‚è¿°
本方案通过多进程实现两个USB摄像头同时工作。每个摄像头运行在独立的进程中,每个进程拥有独立的H264Encoder实例,从而避免了底层C库的单实例限制。
## æž¶æž„说明
- **H264EncodeService**:主进程服务,处理第一个摄像头(cameraId=1)
- **H264EncodeService2**:独立进程服务(`:camera2`),处理第二个摄像头(cameraId=2)
## ä½¿ç”¨æ–¹æ³•
### å¯åŠ¨ç¬¬ä¸€ä¸ªæ‘„åƒå¤´ï¼ˆcameraId=1)
```json
{
  "width": 640,
  "height": 480,
  "framerate": 25,
  "cameraId": 1
}
```
或者不指定cameraId(默认为1):
```json
{
  "width": 640,
  "height": 480,
  "framerate": 25
}
```
### å¯åŠ¨ç¬¬äºŒä¸ªæ‘„åƒå¤´ï¼ˆcameraId=2)
```json
{
  "width": 640,
  "height": 480,
  "framerate": 25,
  "cameraId": 2
}
```
### ç½‘络推送示例
**第一个摄像头:**
```json
{
  "ip": "192.168.1.100",
  "port": 8888,
  "width": 640,
  "height": 480,
  "framerate": 25,
  "simPhone": "013120122580",
  "cameraId": 1
}
```
**第二个摄像头:**
```json
{
  "ip": "192.168.1.100",
  "port": 8889,
  "width": 640,
  "height": 480,
  "framerate": 25,
  "simPhone": "013120122580",
  "cameraId": 2
}
```
### åœæ­¢æŒ‡å®šæ‘„像头
停止第一个摄像头:
```json
{
  "cameraId": 1
}
```
停止第二个摄像头:
```json
{
  "cameraId": 2
}
```
如果不指定cameraId,默认停止第一个摄像头。
## å…³é”®ç‰¹æ€§
1. **进程隔离**:两个摄像头运行在完全独立的进程中,互不干扰
2. **自动路由**:主服务根据cameraId自动路由到对应的进程
3. **独立配置**:每个摄像头可以独立配置分辨率、帧率、网络参数等
4. **独立文件**:第二个摄像头的文件会添加`camera2`标识,便于区分
5. **独立通道**:第二个摄像头使用`logicalChannelId=2`,第一个使用`logicalChannelId=1`
## æ–‡ä»¶å‘½å
- ç¬¬ä¸€ä¸ªæ‘„像头:`h264_1234567890123.h264`
- ç¬¬äºŒä¸ªæ‘„像头:`h264_camera2_1234567890123.h264`
## æ³¨æ„äº‹é¡¹
1. ç¡®ä¿ä¸¤ä¸ªUSB摄像头都已正确连接
2. ç¬¬äºŒä¸ªæ‘„像头固定使用cameraId=2,第一个摄像头使用cameraId=1(或不指定)
3. ä¸¤ä¸ªè¿›ç¨‹å¯ä»¥åŒæ—¶è¿è¡Œï¼Œäº’不影响
4. æ¯ä¸ªè¿›ç¨‹éƒ½æœ‰ç‹¬ç«‹çš„H264Encoder实例,因此可以真正并发工作
## æŠ€æœ¯å®žçް
- ä¸»æœåŠ¡é€šè¿‡AIDL绑定到第二个进程的服务
- ç¬¬äºŒä¸ªè¿›ç¨‹é€šè¿‡`android:process=":camera2"`配置在独立进程中运行
- æ‰€æœ‰æ“ä½œéƒ½é€šè¿‡AIDL接口进行进程间通信