1 文件已重命名
15个文件已修改
6个文件已添加
1500 ■■■■ 已修改文件
app/build.gradle 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/aidl/com/anyun/h264/IH264EncodeService.aidl 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/FileLoggingTree.java 146 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264Application.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264EncodeService.java 276 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264Encoder.java 149 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/H264FileTransmitter.java 38 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/JT1076TcpClient.java 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/MainActivity.kt 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/model/WatermarkInfo.java 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/anyun/h264/service/H264EncodeServiceClient.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
gradle/libs.versions.toml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/CMakeLists.txt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/build.gradle 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/src/main/cpp/ImageProc.c 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/src/main/cpp/apptimer.cpp 262 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/src/main/cpp/apptimer.h 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/src/main/cpp/charencode.cpp 204 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/src/main/cpp/watermark.cpp 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/src/main/cpp/watermark.h 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle
@@ -68,6 +68,7 @@
    implementation libs.androidx.ui.tooling.preview
    implementation libs.androidx.material3
    implementation libs.netty.all
    implementation libs.timber
    testImplementation libs.junit
    androidTestImplementation libs.androidx.junit
    androidTestImplementation libs.androidx.espresso.core
app/src/main/AndroidManifest.xml
@@ -7,10 +7,15 @@
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- 存储权限(用于日志文件) -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <!-- USB摄像头权限 -->
    <uses-feature android:name="android.hardware.usb.host" />
    <application
        android:name=".H264Application"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
app/src/main/aidl/com/anyun/h264/IH264EncodeService.aidl
@@ -33,5 +33,11 @@
     * @return 资源列表(根据JT/T 1076-2016表23定义)
     */
    List<ResourceInfo> getResourceList(String startTime, String endTime);
    /**
     * 设置水印信息
     * @param watermarkInfo 水印信息字符串
     */
    void setWatermarkInfo(String watermarkInfo);
}
app/src/main/java/com/anyun/h264/FileLoggingTree.java
New file
@@ -0,0 +1,146 @@
package com.anyun.h264;
import android.os.Environment;
import android.util.Log;
import timber.log.Timber;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
/**
 * Timber Tree implementation that logs to files.
 * Logs are saved to sdcard/nvlog/h264_yyyyMMdd.log format.
 */
public class FileLoggingTree extends Timber.DebugTree {
    private static final String LOG_DIR = "nvlog";
    private static final String LOG_PREFIX = "h264_";
    private static final String LOG_SUFFIX = ".log";
    private static final String DATE_FORMAT = "yyyyMMdd";
    private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
    private String currentLogFile = null;
    private SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.CHINA);
    private SimpleDateFormat timestampFormat = new SimpleDateFormat(TIMESTAMP_FORMAT, Locale.CHINA);
    @Override
    protected void log(int priority, String tag, String message, Throwable t) {
        try {
            // Get today's log file path
            String today = dateFormat.format(new Date());
            String logFileName = LOG_PREFIX + today + LOG_SUFFIX;
            // Check if we need to update the log file (new day)
            if (!logFileName.equals(currentLogFile)) {
                currentLogFile = logFileName;
            }
            File logFile = getLogFile(logFileName);
            if (logFile == null) {
                return;
            }
            // Format log entry
            String logEntry = formatLogEntry(priority, tag, message, t);
            // Write to file (append mode)
            synchronized (this) {
                try (FileWriter writer = new FileWriter(logFile, true)) {
                    writer.append(logEntry);
                    writer.append("\n");
                    writer.flush();
                } catch (IOException e) {
                    // If file writing fails, log to system log as fallback
                    Log.e("FileLoggingTree", "Failed to write log to file", e);
                }
            }
        } catch (Exception e) {
            // If anything goes wrong, log to system log
            Log.e("FileLoggingTree", "Error in FileLoggingTree", e);
        }
    }
    /**
     * Get the log file. Creates directory if needed.
     */
    private File getLogFile(String fileName) {
        try {
            File logDir = new File(Environment.getExternalStorageDirectory(), LOG_DIR);
            if (!logDir.exists()) {
                if (!logDir.mkdirs()) {
                    Log.e("FileLoggingTree", "Failed to create log directory: " + logDir.getAbsolutePath());
                    return null;
                }
            }
            File logFile = new File(logDir, fileName);
            if (!logFile.exists()) {
                if (!logFile.createNewFile()) {
                    Log.e("FileLoggingTree", "Failed to create log file: " + logFile.getAbsolutePath());
                    return null;
                }
            }
            return logFile;
        } catch (IOException e) {
            Log.e("FileLoggingTree", "Error getting log file", e);
            return null;
        }
    }
    /**
     * Format a log entry with timestamp, priority, tag, and message.
     */
    private String formatLogEntry(int priority, String tag, String message, Throwable t) {
        StringBuilder sb = new StringBuilder();
        // Timestamp
        sb.append(timestampFormat.format(new Date()));
        sb.append(" ");
        // Priority level
        sb.append(getPriorityString(priority));
        sb.append("/");
        // Tag
        sb.append(tag);
        sb.append(": ");
        // Message
        sb.append(message);
        // Throwable stack trace
        if (t != null) {
            sb.append("\n");
            sb.append(Log.getStackTraceString(t));
        }
        return sb.toString();
    }
    /**
     * Get priority string representation.
     */
    private String getPriorityString(int priority) {
        switch (priority) {
            case Log.VERBOSE:
                return "V";
            case Log.DEBUG:
                return "D";
            case Log.INFO:
                return "I";
            case Log.WARN:
                return "W";
            case Log.ERROR:
                return "E";
            case Log.ASSERT:
                return "A";
            default:
                return "?";
        }
    }
}
app/src/main/java/com/anyun/h264/H264Application.java
New file
@@ -0,0 +1,27 @@
package com.anyun.h264;
import android.app.Application;
import timber.log.Timber;
/**
 * Application class for initializing Timber logging.
 */
public class H264Application extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // Initialize Timber
//        if (BuildConfig.DEBUG) {
            // In debug mode, use both console and file logging
            Timber.plant(new Timber.DebugTree());
            Timber.plant(new FileLoggingTree());
//        } else {
//            // In release mode, only use file logging
//            Timber.plant(new FileLoggingTree());
//        }
        Timber.d("H264Application onCreate - Timber initialized");
    }
}
app/src/main/java/com/anyun/h264/H264EncodeService.java
@@ -4,9 +4,10 @@
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import timber.log.Timber;
import com.anyun.h264.model.ResourceInfo;
import com.anyun.h264.model.WatermarkInfo;
import org.json.JSONException;
import org.json.JSONObject;
@@ -29,6 +30,7 @@
    private H264Encoder h264Encoder;
    private H264FileTransmitter h264FileTransmitter; // H264文件传输器
    private String outputFileDirectory; // H264文件输出目录
    private WatermarkInfo currentWatermarkInfo; // 当前水印信息
    
    // 默认编码参数
    private static final int DEFAULT_WIDTH = 640;
@@ -51,27 +53,32 @@
        public List<ResourceInfo> getResourceList(String startTime, String endTime) throws RemoteException {
            return H264EncodeService.this.getResourceList(startTime, endTime);
        }
        @Override
        public void setWatermarkInfo(String watermarkInfo) throws RemoteException {
            H264EncodeService.this.setWatermarkInfo(watermarkInfo);
        }
    };
    
    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "H264EncodeService created");
        Timber.d("H264EncodeService created");
        
        // 初始化输出文件目录(使用应用外部存储目录)
        outputFileDirectory = getExternalFilesDir(null).getAbsolutePath();
        Log.d(TAG, "Output file directory: " + outputFileDirectory);
        Timber.d("Output file directory: %s", outputFileDirectory);
    }
    
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "Service bound");
        Timber.d("Service bound");
        return binder;
    }
    
    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG, "Service unbound");
        Timber.d("Service unbound");
        // 不自动停止编码器,让它在服务中保持运行
        return super.onUnbind(intent);
    }
@@ -79,7 +86,7 @@
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "Service destroyed");
        Timber.d("Service destroyed");
        
        // 停止并释放编码器和文件传输器
        stopEncoder();
@@ -170,7 +177,7 @@
     * @return 0-成功,1-失败
     */
    private synchronized int controlEncode(int action, String jsonConfig) {
        Log.d(TAG, "controlEncode called with action: " + action + ", jsonConfig: " + jsonConfig);
        Timber.d("controlEncode called with action: %d, jsonConfig: %s", action, jsonConfig);
        
        try {
            switch (action) {
@@ -179,7 +186,7 @@
                        EncodeConfig config0 = EncodeConfig.fromJson(jsonConfig);
                        return startFileEncode(config0);
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to parse JSON config: " + jsonConfig, e);
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
                    }
                    
@@ -191,7 +198,7 @@
                        EncodeConfig config2 = EncodeConfig.fromJson(jsonConfig);
                        return startNetworkEncode(config2);
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to parse JSON config: " + jsonConfig, e);
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
                    }
                    
@@ -203,19 +210,20 @@
                        FileTransmitConfig config4 = FileTransmitConfig.fromJson(jsonConfig);
                        return startFileTransmit(config4);
                    } catch (JSONException e) {
                        Log.e(TAG, "Failed to parse JSON config: " + jsonConfig, e);
                        Timber.e(e, "Failed to parse JSON config: %s", jsonConfig);
                        return 1;
                    }
                    
                case 5: // 停止H264文件传输
                    Timber.i("客户端请求停止视频文件上传");
                    return stopFileTransmitter();
                    
                default:
                    Log.e(TAG, "Unknown action: " + action);
                    Timber.e("Unknown action: %d", action);
                    return 1; // 失败
            }
        } catch (Exception e) {
            Log.e(TAG, "Error in controlEncode", e);
            Timber.e(e, "Error in controlEncode");
            return 1; // 失败
        }
    }
@@ -224,11 +232,11 @@
     * 启动文件编码模式(只写入文件,不进行网络推送)
     */
    private int startFileEncode(EncodeConfig config) {
        Log.d(TAG, "Starting file encode mode");
        Timber.d("Starting file encode mode");
        
        // 如果编码器已经在运行,先停止
        if (h264Encoder != null) {
            Log.w(TAG, "Encoder is already running, stopping it first");
            Timber.w("Encoder is already running, stopping it first");
            stopEncoder();
        }
        
@@ -245,7 +253,7 @@
            long timeFile = System.currentTimeMillis();
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss");
            String str = bcdFormat.format(timeFile);
            Log.i(TAG,"文件名:"+str);
            Timber.i("文件名:%s", str);
            // 设置输出文件
            String fileName = "h264_" + timeFile+ ".h264";
            File outputFile = new File(outputFileDirectory, fileName);
@@ -258,17 +266,22 @@
            // 初始化并启动(使用配置中的分辨率)
            int[] resolution = {width, height};
            if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) {
                // 应用已保存的水印信息(如果有)
                if (currentWatermarkInfo != null) {
                    h264Encoder.setWatermarkInfo(currentWatermarkInfo);
                    Timber.d("Applied saved watermark info to encoder");
                }
                h264Encoder.start();
                Log.d(TAG, "File encode started successfully, output file: " + outputFile.getAbsolutePath() +
                        ", resolution: " + width + "x" + height + ", framerate: " + framerate);
                Timber.d("File encode started successfully, output file: %s, resolution: %dx%d, framerate: %d",
                        outputFile.getAbsolutePath(), width, height, framerate);
                return 0; // 成功
            } else {
                Log.e(TAG, "Failed to initialize encoder");
                Timber.e("Failed to initialize encoder");
                h264Encoder = null;
                return 1; // 失败
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to start file encode", e);
            Timber.e(e, "Failed to start file encode");
            h264Encoder = null;
            return 1; // 失败
        }
@@ -278,17 +291,17 @@
     * 启动网络推送模式(只进行网络推送,不写入文件)
     */
    private int startNetworkEncode(EncodeConfig config) {
        Log.d(TAG, "Starting network encode mode");
        Timber.d("Starting network encode mode");
        
        // 如果编码器已经在运行,先停止
        if (h264Encoder != null) {
            Log.w(TAG, "Encoder is already running, stopping it first");
            Timber.w("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");
            Timber.e("Network encode requires valid ip and port in config");
            return 1; // 失败
        }
        
@@ -300,9 +313,9 @@
            // 设置编码参数(使用配置中的参数)
            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 =  DEFAULT_WIDTH;
            int height =  DEFAULT_HEIGHT;
            int framerate = DEFAULT_FRAME_RATE;
            h264Encoder.setEncoderParams(width, height, framerate, DEFAULT_BITRATE);
            
            // 禁用文件输出
@@ -320,17 +333,22 @@
            // 初始化并启动(使用配置中的分辨率)
            int[] resolution = {width, height};
            if (h264Encoder.initialize(DEFAULT_CAMERA_ID_RANGE, null, resolution, false)) {
                // 应用已保存的水印信息(如果有)
                if (currentWatermarkInfo != null) {
                    h264Encoder.setWatermarkInfo(currentWatermarkInfo);
                    Timber.d("Applied saved watermark info to encoder");
                }
                h264Encoder.start();
                Log.d(TAG, "Network encode started successfully, server: " + config.ip + ":" + config.port +
                        ", resolution: " + width + "x" + height + ", framerate: " + framerate);
                Timber.d("Network encode started successfully, server: %s:%d, resolution: %dx%d, framerate: %d",
                        config.ip, config.port, width, height, framerate);
                return 0; // 成功
            } else {
                Log.e(TAG, "Failed to initialize encoder");
                Timber.e("Failed to initialize encoder");
                h264Encoder = null;
                return 1; // 失败
            }
        } catch (Exception e) {
            Log.e(TAG, "Failed to start network encode", e);
            Timber.e(e, "Failed to start network encode");
            h264Encoder = null;
            return 1; // 失败
        }
@@ -340,22 +358,22 @@
     * 停止编码器
     */
    private int stopEncoder() {
        Log.d(TAG, "Stopping encoder");
        Timber.d("Stopping encoder");
        
        if (h264Encoder != null) {
            try {
                h264Encoder.stop();
                h264Encoder.release();
                h264Encoder = null;
                Log.d(TAG, "Encoder stopped successfully");
                Timber.d("Encoder stopped successfully");
                return 0; // 成功
            } catch (Exception e) {
                Log.e(TAG, "Error stopping encoder", e);
                Timber.e(e, "Error stopping encoder");
                h264Encoder = null;
                return 1; // 失败
            }
        } else {
            Log.w(TAG, "Encoder is not running");
            Timber.w("Encoder is not running");
            return 0; // 成功(没有运行的编码器,视为成功)
        }
    }
@@ -364,22 +382,22 @@
     * 启动文件传输模式(从H264文件读取并网络推送)
     */
    private int startFileTransmit(FileTransmitConfig config) {
        Log.d(TAG, "Starting file transmit mode");
        Timber.d("Starting file transmit mode");
        
        // 如果文件传输器已经在运行,先停止
        if (h264FileTransmitter != null) {
            Log.w(TAG, "File transmitter is already running, stopping it first");
            Timber.w("File transmitter is already running, stopping it first");
            stopFileTransmitter();
        }
        
        // 检查必需的配置参数
        if (config == null || config.ip == null || config.ip.trim().isEmpty() || config.port <= 0) {
            Log.e(TAG, "File transmit requires valid ip and port in config");
            Timber.e("File transmit requires valid ip and port in config");
            return 1; // 失败
        }
        
        if (config.filePath == null || config.filePath.trim().isEmpty()) {
            Log.e(TAG, "File transmit requires valid filePath in config");
            Timber.e("File transmit requires valid filePath in config");
            return 1; // 失败
        }
        
@@ -387,7 +405,7 @@
            // 检查文件是否存在
            File file = new File(config.filePath);
            if (!file.exists() || !file.isFile()) {
                Log.e(TAG, "File does not exist: " + config.filePath);
                Timber.e("File does not exist: %s", config.filePath);
                return 1; // 失败
            }
            
@@ -398,7 +416,7 @@
            h264FileTransmitter.setServerAddress(config.ip, config.port);
            
            // 设置协议类型
            h264FileTransmitter.setProtocolType(config.protocolType);
            h264FileTransmitter.setProtocolType(1); //1-tcp
            
            // 设置协议参数(SIM卡号和逻辑通道号)
            String simPhone = config.simPhone != null && !config.simPhone.trim().isEmpty() 
@@ -413,24 +431,25 @@
            h264FileTransmitter.setOnTransmitProgressCallback(new H264FileTransmitter.OnTransmitProgressCallback() {
                @Override
                public void onProgress(int currentFrame, int totalFrames) {
                    Log.d(TAG, "File transmit progress: frame " + currentFrame +
                            (totalFrames > 0 ? " of " + totalFrames : ""));
                    Timber.d("File transmit progress: frame %d%s", currentFrame,
                            totalFrames > 0 ? " of " + totalFrames : "");
                }
                
                @Override
                public void onComplete() {
                    Log.d(TAG, "File transmit completed");
                    Timber.d("File transmit completed");
                    stopFileTransmitter();
                }
                
                @Override
                public void onError(String error) {
                    Log.e(TAG, "File transmit error: " + error);
                    Timber.e("File transmit error: %s", error);
                }
            });
            
            // 初始化Socket连接
            if (!h264FileTransmitter.initialize()) {
                Log.e(TAG, "Failed to initialize file transmitter socket");
                Timber.e("Failed to initialize file transmitter socket");
                h264FileTransmitter = null;
                return 1; // 失败
            }
@@ -438,19 +457,18 @@
            // 开始传输文件
            h264FileTransmitter.transmitFile(config.filePath);
            
            Log.d(TAG, "File transmit started successfully, file: " + config.filePath +
                    ", server: " + config.ip + ":" + config.port +
                    ", protocol: " + (config.protocolType == JT1076ProtocolHelper.PROTOCOL_TYPE_UDP ? "UDP" : "TCP") +
                    ", framerate: " + framerate);
            Timber.d("File transmit started successfully, 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) {
            Log.e(TAG, "Failed to start file transmit", e);
            Timber.e(e, "Failed to start file transmit");
            if (h264FileTransmitter != null) {
                try {
                    h264FileTransmitter.stop();
                } catch (Exception ex) {
                    Log.e(TAG, "Error stopping file transmitter after failure", ex);
                    Timber.e(ex, "Error stopping file transmitter after failure");
                }
                h264FileTransmitter = null;
            }
@@ -462,21 +480,21 @@
     * 停止文件传输器
     */
    private int stopFileTransmitter() {
        Log.d(TAG, "Stopping file transmitter");
        Timber.d("Stopping file transmitter");
        
        if (h264FileTransmitter != null) {
            try {
                h264FileTransmitter.stop();
                h264FileTransmitter = null;
                Log.d(TAG, "File transmitter stopped successfully");
                Timber.d("File transmitter stopped successfully");
                return 0; // 成功
            } catch (Exception e) {
                Log.e(TAG, "Error stopping file transmitter", e);
                Timber.e(e, "Error stopping file transmitter");
                h264FileTransmitter = null;
                return 1; // 失败
            }
        } else {
            Log.w(TAG, "File transmitter is not running");
            Timber.w("File transmitter is not running");
            return 0; // 成功(没有运行的文件传输器,视为成功)
        }
    }
@@ -488,7 +506,7 @@
     * @return 资源列表
     */
    private List<ResourceInfo> getResourceList(String startTime, String endTime) {
        Log.d(TAG, "getResourceList called, startTime: " + startTime + ", endTime: " + endTime);
        Timber.d("getResourceList called, startTime: %s, endTime: %s", startTime, endTime);
        
        List<ResourceInfo> resourceList = new ArrayList<>();
        
@@ -496,13 +514,13 @@
            // 扫描输出目录中的H264文件
            File dir = new File(outputFileDirectory);
            if (!dir.exists() || !dir.isDirectory()) {
                Log.w(TAG, "Output directory does not exist: " + outputFileDirectory);
                Timber.w("Output directory does not exist: %s", outputFileDirectory);
                return resourceList;
            }
            
            File[] files = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".h264"));
            if (files == null || files.length == 0) {
                Log.d(TAG, "No H264 files found in directory");
                Timber.d("No H264 files found in directory");
                return resourceList;
            }
            
@@ -511,7 +529,7 @@
            Date endDate = parseTime(endTime);
            
            if (startDate == null || endDate == null) {
                Log.e(TAG, "Invalid time format, startTime: " + startTime + ", endTime: " + endTime);
                Timber.e("Invalid time format, startTime: %s, endTime: %s", startTime, endTime);
                return resourceList;
            }
            
@@ -523,12 +541,106 @@
                }
            }
            
            Log.d(TAG, "Found " + resourceList.size() + " resources in time range");
            Timber.d("Found %d resources in time range", resourceList.size());
            return resourceList;
            
        } catch (Exception e) {
            Log.e(TAG, "Error getting resource list", e);
            Timber.e(e, "Error getting resource list");
            return resourceList;
        }
    }
    /**
     * 设置水印信息
     * @param watermarkInfoJson 水印信息JSON字符串,包含:车牌(plateNumber)、学员(student)、教练(coach)、
     *                          经度(longitude)、纬度(latitude)、驾校(drivingSchool)、车速(speed)
     *                          示例:{"plateNumber":"京A12345","student":"张三","coach":"李四",
     *                                "longitude":116.397128,"latitude":39.916527,"drivingSchool":"XX驾校","speed":60.5}
     */
    private void setWatermarkInfo(String watermarkInfoJson) {
        Timber.d("setWatermarkInfo called, watermarkInfoJson: %s", watermarkInfoJson);
        try {
            if (watermarkInfoJson == null || watermarkInfoJson.trim().isEmpty()) {
                Timber.w("Watermark info JSON is null or empty, clearing watermark");
                currentWatermarkInfo = null;
                // 如果编码器正在运行,清除水印
                if (h264Encoder != null) {
                    h264Encoder.setWatermarkInfo(null);
                }
                return;
            }
            // 解析JSON
            JSONObject json = new JSONObject(watermarkInfoJson);
            WatermarkInfo watermarkInfo = new WatermarkInfo();
            // 解析各个字段(使用 optString/optDouble 避免字段不存在时抛出异常)
            watermarkInfo.setPlateNumber(json.optString("plateNumber", null));
            watermarkInfo.setStudent(json.optString("student", null));
            watermarkInfo.setCoach(json.optString("coach", null));
            // 经度和纬度可能是数字或字符串
            if (json.has("longitude")) {
                Object lonObj = json.get("longitude");
                if (lonObj instanceof Number) {
                    watermarkInfo.setLongitude(((Number) lonObj).doubleValue());
                } else if (lonObj instanceof String) {
                    try {
                        watermarkInfo.setLongitude(Double.parseDouble((String) lonObj));
                    } catch (NumberFormatException e) {
                        Timber.w("Invalid longitude format: %s", lonObj);
                    }
                }
            }
            if (json.has("latitude")) {
                Object latObj = json.get("latitude");
                if (latObj instanceof Number) {
                    watermarkInfo.setLatitude(((Number) latObj).doubleValue());
                } else if (latObj instanceof String) {
                    try {
                        watermarkInfo.setLatitude(Double.parseDouble((String) latObj));
                    } catch (NumberFormatException e) {
                        Timber.w("Invalid latitude format: %s", latObj);
                    }
                }
            }
            watermarkInfo.setDrivingSchool(json.optString("drivingSchool", null));
            // 车速可能是数字或字符串
            if (json.has("speed")) {
                Object speedObj = json.get("speed");
                if (speedObj instanceof Number) {
                    watermarkInfo.setSpeed(((Number) speedObj).doubleValue());
                } else if (speedObj instanceof String) {
                    try {
                        watermarkInfo.setSpeed(Double.parseDouble((String) speedObj));
                    } catch (NumberFormatException e) {
                        Timber.w("Invalid speed format: %s", speedObj);
                    }
                }
            }
            // 保存水印信息
            currentWatermarkInfo = watermarkInfo;
            Timber.i("Watermark info parsed successfully: %s", watermarkInfo);
            // 如果编码器正在运行,立即应用水印
            if (h264Encoder != null) {
                h264Encoder.setWatermarkInfo(watermarkInfo);
                Timber.d("Watermark applied to encoder");
            } else {
                Timber.d("Encoder not running, watermark will be applied when encoder starts");
            }
        } catch (JSONException e) {
            Timber.e(e, "Failed to parse watermark info JSON: %s", watermarkInfoJson);
            currentWatermarkInfo = null;
        } catch (Exception e) {
            Timber.e(e, "Unexpected error setting watermark info");
            currentWatermarkInfo = null;
        }
    }
    
@@ -537,12 +649,33 @@
     */
    private ResourceInfo createResourceInfoFromFile(File file, Date startDate, Date endDate) {
        try {
            // 从文件名或文件修改时间获取文件时间
            // 这里假设文件名包含时间戳,或者使用文件修改时间
            Date fileDate = new Date(file.lastModified());
            // 从文件名中提取时间戳(格式:h264_1234567890123.h264)
            String fileName = file.getName();
            Date startTimeFromFileName = null;
            if (fileName.startsWith("h264_") && fileName.endsWith(".h264")) {
                try {
                    // 提取文件名中的时间戳(h264_ 和 .h264 之间的部分)
                    String timestampStr = fileName.substring(5, fileName.length() - 5); // 去掉 "h264_" 和 ".h264"
                    long timestamp = Long.parseLong(timestampStr);
                    startTimeFromFileName = new Date(timestamp);
                } catch (NumberFormatException e) {
                    Timber.w("Failed to parse timestamp from filename: %s", fileName);
                }
            }
            // 如果无法从文件名解析时间戳,则使用文件修改时间作为开始时间
            if (startTimeFromFileName == null) {
                startTimeFromFileName = new Date(file.lastModified());
            }
            // 结束时间使用文件修改时间
            Date endTimeFromFile = new Date(file.lastModified());
            
            // 检查文件时间是否在指定范围内
            if (fileDate.before(startDate) || fileDate.after(endDate)) {
            // 开始时间应该 >= startDate,结束时间应该 <= endDate
            // 如果文件的开始时间在范围内,或者结束时间在范围内,或者文件时间范围包含查询范围,则包含该文件
            if (startTimeFromFileName.after(endDate) || endTimeFromFile.before(startDate)) {
                return null; // 不在时间范围内
            }
            
@@ -552,13 +685,12 @@
            // 逻辑通道号(默认值,实际应从配置获取)
            resourceInfo.setLogicalChannelNumber((byte) 1);
            
            // 开始时间和结束时间(使用文件修改时间)
            // 开始时间:从文件名中的时间戳
            SimpleDateFormat bcdFormat = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            resourceInfo.setStartTime(bcdFormat.format(fileDate));
            // 结束时间可以使用文件修改时间加上一个默认时长(例如1分钟)
            long fileDuration = 60000; // 默认1分钟,实际应该根据文件内容计算
            Date endTime = new Date(fileDate.getTime() + fileDuration);
            resourceInfo.setEndTime(bcdFormat.format(endTime));
            resourceInfo.setStartTime(bcdFormat.format(startTimeFromFileName));
            // 结束时间:使用文件修改时间
            resourceInfo.setEndTime(bcdFormat.format(endTimeFromFile));
            
            // 报警标志(默认值,实际应从文件元数据获取)
            resourceInfo.setAlarmFlag(0L);
@@ -578,7 +710,7 @@
            return resourceInfo;
            
        } catch (Exception e) {
            Log.e(TAG, "Error creating resource info from file: " + file.getName(), e);
            Timber.e(e, "Error creating resource info from file: %s", file.getName());
            return null;
        }
    }
@@ -597,7 +729,7 @@
            SimpleDateFormat format = new SimpleDateFormat("yyMMddHHmmss", Locale.CHINA);
            return format.parse(timeStr);
        } catch (ParseException e) {
            Log.e(TAG, "Failed to parse time: " + timeStr, e);
            Timber.e(e, "Failed to parse time: %s", timeStr);
            return null;
        }
    }
app/src/main/java/com/anyun/h264/H264Encoder.java
@@ -3,15 +3,19 @@
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.util.Log;
import com.anyun.libusbcamera.UsbCamera;
import com.anyun.libusbcamera.WatermarkParam;
import com.anyun.h264.model.WatermarkInfo;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicBoolean;
import timber.log.Timber;
/**
 * H264视频编码器
@@ -143,7 +147,88 @@
     */
    public void setEnableNetworkTransmission(boolean enable) {
        this.enableNetworkTransmission = enable;
        Log.d(TAG, "Network transmission " + (enable ? "enabled" : "disabled"));
        Timber.d("Network transmission " + (enable ? "enabled" : "disabled"));
    }
    /**
     * 设置水印信息
     * @param watermarkInfo 水印信息对象
     */
    public void setWatermarkInfo(WatermarkInfo watermarkInfo) {
        if (watermarkInfo == null) {
            Timber.w("WatermarkInfo is null, disabling watermark");
            usbCamera.enableWatermark(false, null);
            return;
        }
        try {
            // 构建水印文本列表(分行显示,每行一个信息项)
            ArrayList<WatermarkParam> watermarkParams = new ArrayList<>();
            // 从左上角开始,每行间隔25像素
            int yOffset = 30;
            int xOffset = 10;
            // 车牌号
            if (watermarkInfo.getPlateNumber() != null && !watermarkInfo.getPlateNumber().isEmpty()) {
                watermarkParams.add(new WatermarkParam(xOffset, yOffset,
                    "车牌:" + watermarkInfo.getPlateNumber()));
                yOffset += 25;
            }
            // 学员姓名
            if (watermarkInfo.getStudent() != null && !watermarkInfo.getStudent().isEmpty()) {
                watermarkParams.add(new WatermarkParam(xOffset, yOffset,
                    "学员:" + watermarkInfo.getStudent()));
                yOffset += 25;
            }
            // 教练姓名
            if (watermarkInfo.getCoach() != null && !watermarkInfo.getCoach().isEmpty()) {
                watermarkParams.add(new WatermarkParam(xOffset, yOffset,
                    "教练:" + watermarkInfo.getCoach()));
                yOffset += 25;
            }
            // 位置信息(纬度,经度)
            if (watermarkInfo.getLongitude() != null && watermarkInfo.getLatitude() != null) {
                watermarkParams.add(new WatermarkParam(xOffset, yOffset,
                    String.format("位置:%.6f,%.6f", watermarkInfo.getLatitude(), watermarkInfo.getLongitude())));
                yOffset += 25;
            }
            // 驾校名称
            if (watermarkInfo.getDrivingSchool() != null && !watermarkInfo.getDrivingSchool().isEmpty()) {
                watermarkParams.add(new WatermarkParam(xOffset, yOffset,
                    "驾校:" + watermarkInfo.getDrivingSchool()));
                yOffset += 25;
            }
            // 车速
            if (watermarkInfo.getSpeed() != null) {
                watermarkParams.add(new WatermarkParam(xOffset, yOffset,
                    String.format("车速:%.1fkm/h", watermarkInfo.getSpeed())));
            }
            if (!watermarkParams.isEmpty()) {
                // 启用水印,使用默认字体路径(如果系统有字体文件)
                // 颜色:0-REVERSE(反色),1-BLACK,2-WHITE,3-RED,4-GREEN,5-BLUE
                // 字体大小、倍数可以根据需要调整
                String fontPath = "/system/fonts/DroidSans.ttf"; // 默认字体路径,如果不存在可以传null
                usbCamera.enableWatermark(true, fontPath);
                // 设置水印:颜色(2=白色),字体大小(24),倍数(1),文本列表
                usbCamera.setWatermark(2, 24, 1, watermarkParams);
                Timber.d("Watermark set successfully: %s", watermarkInfo);
            } else {
                Timber.w("No watermark text to display, disabling watermark");
                usbCamera.enableWatermark(false, null);
            }
        } catch (Exception e) {
            Timber.e(e, "Failed to set watermark");
            usbCamera.enableWatermark(false, null);
        }
    }
    /**
@@ -167,27 +252,27 @@
                if (result == 0) {
                    // 成功,跳出循环
                    if (attempt > 0) {
                        Log.d(TAG, "prepareCamera succeeded on attempt " + (attempt + 1));
                        Timber.d("prepareCamera succeeded on attempt " + (attempt + 1));
                    }
                    break;
                } else {
                    // 失败,记录日志
                    Log.w(TAG, "prepareCamera failed on attempt " + (attempt + 1) + ": " + result);
                    Timber.w( "prepareCamera failed on attempt " + (attempt + 1) + ": " + result);
                    if (attempt < maxRetries - 1) {
                        Log.d(TAG, "Retrying prepareCamera...");
                        Timber.d( "Retrying prepareCamera...");
                    }
                }
            }
            if (result != 0) {
                Log.e(TAG, "prepareCamera failed after " + maxRetries + " attempts: " + result);
                Timber.e("prepareCamera failed after " + maxRetries + " attempts: " + result);
                return false;
            }
            // 更新实际分辨率
            width = actualResolution[0];
            height = actualResolution[1];
            Log.d(TAG, "Camera initialized with resolution: " + width + "x" + height);
            Timber.d("Camera initialized with resolution: " + width + "x" + height);
            // 3. 初始化H264编码器
            initEncoder();
@@ -199,19 +284,19 @@
                    return false;
                }
            } else {
                Log.d(TAG, "Network transmission disabled, skipping socket initialization");
                Timber.d("Network transmission disabled, skipping socket initialization");
            }
            // 5. 初始化文件输出(仅创建文件,SPS/PPS在第一次输出时写入)
            if (enableFileOutput && outputFilePath != null && !outputFilePath.isEmpty()) {
                if (!initFileOutput()) {
                    Log.w(TAG, "File output initialization failed, continuing without file output");
                    Timber.w("File output initialization failed, continuing without file output");
                }
            }
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Initialize failed", e);
            Timber.e(e,"Initialize failed");
            return false;
        }
    }
@@ -230,7 +315,7 @@
        encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
        encoder.start();
        Log.d(TAG, "H264 encoder initialized");
        Timber.d( "H264 encoder initialized");
    }
    /**
@@ -244,7 +329,7 @@
            if (parentDir != null && !parentDir.exists()) {
                boolean created = parentDir.mkdirs();
                if (!created && !parentDir.exists()) {
                    Log.e(TAG, "Failed to create parent directory: " + parentDir.getAbsolutePath());
                    Timber.e("Failed to create parent directory: " + parentDir.getAbsolutePath());
                    return false;
                }
            }
@@ -252,15 +337,15 @@
            fileOutputStream = new FileOutputStream(file);
            spsPpsWritten = false;
            Log.d(TAG, "File output initialized: " + outputFilePath);
            Timber.d("File output initialized: " + outputFilePath);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Initialize file output failed", e);
            Timber.e(e,"Initialize file output failed");
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException ie) {
                    Log.e(TAG, "Close file output stream failed", ie);
                    Timber.e(ie, "Close file output stream failed");
                }
                fileOutputStream = null;
            }
@@ -321,12 +406,12 @@
                fileOutputStream.flush();
                spsPpsWritten = true;
                Log.d(TAG, "SPS/PPS written to file, SPS size: " + spsLength + ", PPS size: " + ppsLength);
                Timber.d("SPS/PPS written to file, SPS size: " + spsLength + ", PPS size: " + ppsLength);
            } else {
                Log.w(TAG, "SPS/PPS not found in CSD, will extract from first key frame");
                Timber.w("SPS/PPS not found in CSD, will extract from first key frame");
            }
        } catch (Exception e) {
            Log.e(TAG, "Write SPS/PPS to file error", e);
            Timber.e(e,"Write SPS/PPS to file error");
        }
    }
@@ -335,7 +420,7 @@
     */
    public void start() {
        if (isRunning.get()) {
            Log.w(TAG, "Encoder is already running");
            Timber.w("Encoder is already running");
            return;
        }
@@ -350,7 +435,7 @@
        });
        encodeThread.start();
        Log.d(TAG, "H264 encoder started");
        Timber.d("H264 encoder started");
    }
    /**
@@ -371,7 +456,7 @@
                // processCamera - 读取一帧
                int processResult = usbCamera.processCamera();
                if (processResult != 0) {
                    Log.w(TAG, "processCamera returned: " + processResult);
                    Timber.w("processCamera returned: " + processResult);
                    Thread.sleep(10);
                    continue;
                }
@@ -384,7 +469,7 @@
                encodeFrame(nv12ToNV21(yuvBuffer,width,height), timestamp, bufferInfo);
            } catch (Exception e) {
                Log.e(TAG, "Encode loop error", e);
                Timber.e(e, "Encode loop error");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ie) {
@@ -393,7 +478,7 @@
            }
        }
        Log.d(TAG, "Encode loop exited");
        Timber.d( "Encode loop exited");
    }
    /**
@@ -441,7 +526,7 @@
            }
        } catch (Exception e) {
            Log.e(TAG, "Encode frame error", e);
            Timber.e(e,"Encode frame error");
        }
    }
@@ -462,7 +547,7 @@
                // MediaCodec输出的关键帧通常已经包含SPS/PPS,但为了确保文件完整性,
                // 我们已经从CSD写入,这里直接写入关键帧数据即可
                if (!spsPpsWritten) {
                    Log.d(TAG, "SPS/PPS will be included in key frame data");
                    Timber.d("SPS/PPS will be included in key frame data");
                }
            }
@@ -471,7 +556,7 @@
            fileOutputStream.write(data);
            fileOutputStream.flush();
        } catch (IOException e) {
            Log.e(TAG, "Write to file error", e);
            Timber.e(e, "Write to file error");
        }
    }
@@ -531,7 +616,7 @@
            }
        } catch (Exception e) {
            Log.e(TAG, "Send encoded data error", e);
            Timber.e(e,"Send encoded data error");
        }
    }
@@ -550,7 +635,7 @@
            try {
                encodeThread.join(2000);
            } catch (InterruptedException e) {
                Log.e(TAG, "Wait encode thread error", e);
                Timber.e(e, "Wait encode thread error");
            }
        }
@@ -566,7 +651,7 @@
                encoder.release();
                encoder = null;
            } catch (Exception e) {
                Log.e(TAG, "Release encoder error", e);
                Timber.e(e, "Release encoder error");
            }
        }
@@ -579,7 +664,7 @@
        // 关闭文件输出
        closeFileOutput();
        Log.d(TAG, "H264 encoder stopped");
        Timber.d("H264 encoder stopped");
    }
    /**
@@ -590,9 +675,9 @@
            try {
                fileOutputStream.flush();
                fileOutputStream.close();
                Log.d(TAG, "File output closed: " + outputFilePath);
                Timber.d("File output closed: " + outputFilePath);
            } catch (IOException e) {
                Log.e(TAG, "Close file output error", e);
                Timber.e(e, "Close file output error");
            } finally {
                fileOutputStream = null;
                spsPpsWritten = false;
app/src/main/java/com/anyun/h264/H264FileTransmitter.java
@@ -1,6 +1,6 @@
package com.anyun.h264;
import android.util.Log;
import timber.log.Timber;
import java.io.File;
import java.io.FileInputStream;
@@ -102,7 +102,7 @@
     * @param protocolType PROTOCOL_TYPE_UDP 或 PROTOCOL_TYPE_TCP
     */
    public void setProtocolType(int protocolType) {
        protocolHelper.setProtocolType(protocolType);
        protocolHelper.setProtocolType(JT1076ProtocolHelper.PROTOCOL_TYPE_TCP);
    }
    
    /**
@@ -119,7 +119,7 @@
    public void setFrameRate(int frameRate) {
        this.frameRate = frameRate > 0 ? frameRate : 25;
        this.frameInterval = 1000 / this.frameRate;
        Log.d(TAG, "Frame rate set to: " + this.frameRate + " fps, interval: " + this.frameInterval + " ms");
        Timber.d("Frame rate set to: %d fps, interval: %d ms", this.frameRate, this.frameInterval);
    }
    
    /**
@@ -135,12 +135,12 @@
     */
    public boolean initialize() {
        if (isRunning.get()) {
            Log.w(TAG, "Transmitter is already running");
            Timber.w("Transmitter is already running");
            return false;
        }
        
        if (!protocolHelper.initializeSocket()) {
            Log.e(TAG, "Failed to initialize socket");
            Timber.e("Failed to initialize socket");
            return false;
        }
        
@@ -152,7 +152,7 @@
        lastIFrameTime = 0;
        lastFrameTime = 0;
        
        Log.d(TAG, "Socket initialized successfully");
        Timber.d("Socket initialized successfully");
        return true;
    }
    
@@ -162,13 +162,13 @@
     */
    public void transmitFile(String filePath) {
        if (isRunning.get()) {
            Log.w(TAG, "Transmitter is already running");
            Timber.w("Transmitter is already running");
            return;
        }
        
        File file = new File(filePath);
        if (!file.exists() || !file.isFile()) {
            Log.e(TAG, "File does not exist: " + filePath);
            Timber.e("File does not exist: %s", filePath);
            if (progressCallback != null) {
                progressCallback.onError("File does not exist: " + filePath);
            }
@@ -186,7 +186,7 @@
        });
        transmitThread.start();
        
        Log.d(TAG, "Started transmitting file: " + filePath);
        Timber.d("Started transmitting file: %s", filePath);
    }
    
    /**
@@ -214,10 +214,10 @@
            }
            
            if (totalRead != fileData.length) {
                Log.w(TAG, "File read incomplete, expected: " + fileData.length + ", actual: " + totalRead);
                Timber.w("File read incomplete, expected: %d, actual: %d", fileData.length, totalRead);
            }
            
            Log.d(TAG, "File read complete, size: " + fileData.length + " bytes");
            Timber.d("File read complete, size: %d bytes", fileData.length);
            
            // 按帧解析并传输(一个帧包含从一个起始码到下一个起始码之间的所有数据,包括起始码)
            int offset = 0;
@@ -255,19 +255,19 @@
                offset = nextFrameStart;
            }
            
            Log.d(TAG, "Transmission complete, total frames: " + frameCount);
            Timber.d("Transmission complete, total frames: %d", frameCount);
            
            if (progressCallback != null) {
                progressCallback.onComplete();
            }
            
        } catch (IOException e) {
            Log.e(TAG, "Error transmitting file", e);
            Timber.e(e, "Error transmitting file");
            if (progressCallback != null) {
                progressCallback.onError("IO Error: " + e.getMessage());
            }
        } catch (Exception e) {
            Log.e(TAG, "Unexpected error during transmission", e);
            Timber.e(e, "Unexpected error during transmission");
            if (progressCallback != null) {
                progressCallback.onError("Error: " + e.getMessage());
            }
@@ -276,7 +276,7 @@
                try {
                    fis.close();
                } catch (IOException e) {
                    Log.e(TAG, "Error closing file", e);
                    Timber.e(e, "Error closing file");
                }
            }
            isRunning.set(false);
@@ -464,13 +464,13 @@
                    Thread.sleep(frameInterval);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    Log.d(TAG, "Transmission interrupted");
                    Timber.d("Transmission interrupted");
                    return;
                }
            }
            
        } catch (Exception e) {
            Log.e(TAG, "Error transmitting frame", e);
            Timber.e(e, "Error transmitting frame");
        }
    }
    
@@ -489,7 +489,7 @@
            try {
                transmitThread.join(2000);
            } catch (InterruptedException e) {
                Log.e(TAG, "Wait transmit thread error", e);
                Timber.e(e, "Wait transmit thread error");
            }
        }
        
@@ -498,7 +498,7 @@
            protocolHelper.closeSocket();
        }
        
        Log.d(TAG, "H264 file transmitter stopped");
        Timber.d("H264 file transmitter stopped");
    }
    
    /**
app/src/main/java/com/anyun/h264/JT1076ProtocolHelper.java
@@ -1,12 +1,13 @@
package com.anyun.h264;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import timber.log.Timber;
/**
 * JT/T 1076-2016 协议工具类
@@ -46,7 +47,7 @@
    private int serverPort;
    
    // 协议类型(默认UDP)
    private int protocolType = PROTOCOL_TYPE_UDP;
    private int protocolType = PROTOCOL_TYPE_TCP;
    
    // UDP参数
    private DatagramSocket udpSocket;
@@ -78,8 +79,8 @@
     */
    public void setProtocolType(int protocolType) {
        if (protocolType != PROTOCOL_TYPE_UDP && protocolType != PROTOCOL_TYPE_TCP) {
            Log.w(TAG, "Invalid protocol type: " + protocolType + ", using UDP");
            protocolType = PROTOCOL_TYPE_UDP;
            Timber.w("Invalid protocol type: " + protocolType + ", using UDP");
            protocolType = PROTOCOL_TYPE_TCP;
        }
        
        // 如果协议类型改变,先关闭旧的连接
@@ -92,7 +93,7 @@
        }
        
        this.protocolType = protocolType;
        Log.d(TAG, "Protocol type set to: " + (protocolType == PROTOCOL_TYPE_UDP ? "UDP" : "TCP"));
        Timber.d("Protocol type set to: " + (protocolType == PROTOCOL_TYPE_UDP ? "UDP" : "TCP"));
    }
    
    /**
@@ -127,16 +128,16 @@
    public boolean initializeUdpSocket() {
        try {
            if (serverIp == null || serverIp.isEmpty()) {
                Log.e(TAG, "Server IP not set");
                Timber.e("Server IP not set");
                return false;
            }
            
            udpSocket = new DatagramSocket();
            serverAddress = InetAddress.getByName(serverIp);
            Log.d(TAG, "UDP socket initialized, target: " + serverIp + ":" + serverPort);
            Timber.d("UDP socket initialized, target: " + serverIp + ":" + serverPort);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Initialize UDP socket failed", e);
            Timber.e(e,"Initialize UDP socket failed");
            return false;
        }
    }
@@ -147,7 +148,7 @@
    public boolean initializeTcpSocket() {
        try {
            if (serverIp == null || serverIp.isEmpty()) {
                Log.e(TAG, "Server IP not set");
                Timber.e("Server IP not set");
                return false;
            }
            
@@ -159,26 +160,26 @@
                tcpClient.setConnectionListener(new JT1076TcpClient.ConnectionListener() {
                    @Override
                    public void onConnected() {
                        Log.d(TAG, "TCP connection established");
                        Timber.d("TCP connection established");
                    }
                    
                    @Override
                    public void onDisconnected() {
                        Log.d(TAG, "TCP connection disconnected");
                        Timber.d( "TCP connection disconnected");
                    }
                    
                    @Override
                    public void onError(Throwable cause) {
                        Log.e(TAG, "TCP connection error", cause);
                        Timber.e(cause, "TCP connection error");
                    }
                });
            }
            
            tcpClient.connect();
            Log.d(TAG, "TCP socket initializing, target: " + serverIp + ":" + serverPort);
            Timber.d("TCP socket initializing, target: " + serverIp + ":" + serverPort);
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Initialize TCP socket failed", e);
            Timber.e(e,"Initialize TCP socket failed");
            return false;
        }
    }
@@ -202,7 +203,7 @@
            try {
                udpSocket.close();
            } catch (Exception e) {
                Log.e(TAG, "Close UDP socket error", e);
                Timber.e( e,"Close UDP socket error");
            }
            udpSocket = null;
        }
@@ -240,10 +241,10 @@
                    packet, packet.length, serverAddress, serverPort);
                udpSocket.send(datagramPacket);
            } else {
                Log.w(TAG, "UDP socket not initialized");
                Timber.w("UDP socket not initialized");
            }
        } catch (Exception e) {
            Log.e(TAG, "Send UDP packet error", e);
            Timber.e(e,"Send UDP packet error");
        }
    }
    
@@ -254,7 +255,7 @@
        if (tcpClient != null && tcpClient.isConnected()) {
            tcpClient.sendPacket(packet);
        } else {
            Log.w(TAG, "TCP socket not connected");
            Timber.w("TCP socket not connected");
        }
    }
    
app/src/main/java/com/anyun/h264/JT1076TcpClient.java
@@ -1,6 +1,5 @@
package com.anyun.h264;
import android.util.Log;
import com.anyun.h264.util.BytesUtils;
@@ -14,11 +13,11 @@
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import timber.log.Timber;
import java.util.concurrent.TimeUnit;
@@ -64,7 +63,7 @@
     */
    public void connect() {
        if (serverIp == null || serverIp.isEmpty()) {
            Log.e(TAG, "Server IP not set");
            Timber.e( "Server IP not set");
            if (connectionListener != null) {
                connectionListener.onError(new IllegalArgumentException("Server IP not set"));
            }
@@ -72,7 +71,7 @@
        }
        
        if (workerGroup != null) {
            Log.w(TAG, "TCP client already initialized, disconnecting first");
            Timber.w("TCP client already initialized, disconnecting first");
            disconnect();
        }
        
@@ -93,7 +92,7 @@
                        }
                    });
            
            Log.d(TAG, "Connecting to TCP server: " + serverIp + ":" + serverPort);
            Timber.d("Connecting to TCP server: " + serverIp + ":" + serverPort);
            
            // 异步连接
            ChannelFuture future = bootstrap.connect(serverIp, serverPort);
@@ -103,14 +102,14 @@
                    if (future.isSuccess()) {
                        channel = future.channel();
                        isConnected = true;
                        Log.d(TAG, "TCP connection established: " + serverIp + ":" + serverPort);
                        Timber.d("TCP connection established: " + serverIp + ":" + serverPort);
                        if (connectionListener != null) {
                            connectionListener.onConnected();
                        }
                    } else {
                        isConnected = false;
                        Throwable cause = future.cause();
                        Log.e(TAG, "TCP connection failed: " + serverIp + ":" + serverPort, cause);
                        Timber.e(cause, "TCP connection failed: " + serverIp + ":" + serverPort);
                        if (connectionListener != null) {
                            connectionListener.onError(cause);
                        }
@@ -121,7 +120,7 @@
            });
            
        } catch (Exception e) {
            Log.e(TAG, "Initialize TCP client failed", e);
            Timber.e(e, "Initialize TCP client failed");
            isConnected = false;
            if (connectionListener != null) {
                connectionListener.onError(e);
@@ -135,7 +134,7 @@
     */
    public void sendPacket(byte[] packet) {
        if (!isConnected || channel == null || !channel.isActive()) {
            Log.w(TAG, "TCP channel not connected, packet dropped");
            Timber.w( "TCP channel not connected, packet dropped");
            return;
        }
        
@@ -143,7 +142,7 @@
            // 将字节数组包装为ByteBuf
            ByteBuf buffer = Unpooled.wrappedBuffer(packet);
            String str = BytesUtils.bytesToHexString(  BytesUtils.subArray(buffer.array(),0,30));
            Log.i(TAG, "Send TCP packet:"+ str);
            Timber.i( "Send TCP packet:"+ str);
            // 异步写入
            ChannelFuture future = channel.writeAndFlush(buffer);
            
@@ -152,13 +151,13 @@
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!future.isSuccess()) {
                        Log.e(TAG, "Send TCP packet failed", future.cause());
                        Timber.e(future.cause(), "Send TCP packet failed");
                    }
                }
            });
            
        } catch (Exception e) {
            Log.e(TAG, "Send TCP packet error", e);
            Timber.e(e, "Send TCP packet error");
        }
    }
    
@@ -173,10 +172,10 @@
                ChannelFuture future = channel.close();
                future.await(2, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Log.w(TAG, "Close channel interrupted", e);
                Timber.w(e,"Close channel interrupted");
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                Log.e(TAG, "Close channel error", e);
                Timber.e(e, "Close channel error");
            }
            channel = null;
        }
@@ -187,7 +186,7 @@
            connectionListener.onDisconnected();
        }
        
        Log.d(TAG, "TCP connection closed");
        Timber.d("TCP connection closed");
    }
    
    /**
@@ -198,10 +197,10 @@
            try {
                workerGroup.shutdownGracefully().await(3, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Log.w(TAG, "Shutdown worker group interrupted", e);
                Timber.w(e, "Shutdown worker group interrupted");
                Thread.currentThread().interrupt();
            } catch (Exception e) {
                Log.e(TAG, "Shutdown worker group error", e);
                Timber.e(e, "Shutdown worker group error");
            }
            workerGroup = null;
        }
@@ -222,14 +221,14 @@
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            super.channelActive(ctx);
            Log.d(TAG, "TCP channel active: " + ctx.channel().remoteAddress());
            Timber.d("TCP channel active: " + ctx.channel().remoteAddress());
            isConnected = true;
        }
        
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            super.channelInactive(ctx);
            Log.d(TAG, "TCP channel inactive: " + ctx.channel().remoteAddress());
            Timber.d("TCP channel inactive: " + ctx.channel().remoteAddress());
            isConnected = false;
            channel = null;
            if (connectionListener != null) {
@@ -239,7 +238,7 @@
        
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            Log.e(TAG, "TCP channel exception", cause);
            Timber.e(cause, "TCP channel exception");
            isConnected = false;
            if (connectionListener != null) {
                connectionListener.onError(cause);
@@ -252,7 +251,7 @@
            // 如果服务器有响应,可以在这里处理
            // 对于JT/T 1076-2016 RTP发送,通常不需要处理响应
            ByteBuf buf = (ByteBuf) msg;
            Log.d(TAG, "Received data from server: " + buf.readableBytes() + " bytes");
            Timber.d("Received data from server: " + buf.readableBytes() + " bytes");
            buf.release(); // 释放ByteBuf
        }
    }
app/src/main/java/com/anyun/h264/MainActivity.kt
@@ -1,7 +1,7 @@
package com.anyun.h264
import android.os.Bundle
import android.util.Log
import timber.log.Timber
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
@@ -52,7 +52,7 @@
    
    private fun startH264Encoder(): Boolean {
        if (h264Encoder != null) {
            Log.w("MainActivity", "H264Encoder is already running")
            Timber.w("H264Encoder is already running")
            return false
        }
        
@@ -80,16 +80,16 @@
            
            if (h264Encoder?.initialize(cameraIdRange, null, resolution, false) == true) {
                h264Encoder?.start()
                Log.d("MainActivity", "H264Encoder started successfully")
                Log.d("MainActivity", "Output file: ${outputFile.absolutePath}")
                Timber.d("H264Encoder started successfully")
                Timber.d("Output file: %s", outputFile.absolutePath)
                return true
            } else {
                Log.e("MainActivity", "Failed to initialize H264Encoder")
                Timber.e("Failed to initialize H264Encoder")
                h264Encoder = null
                return false
            }
        } catch (e: Exception) {
            Log.e("MainActivity", "Failed to start H264Encoder", e)
            Timber.e(e, "Failed to start H264Encoder")
            h264Encoder = null
            return false
        }
@@ -99,9 +99,9 @@
        h264Encoder?.let { encoder ->
            try {
                encoder.stop()
                Log.d("MainActivity", "H264Encoder stopped")
                Timber.d("H264Encoder stopped")
            } catch (e: Exception) {
                Log.e("MainActivity", "Failed to stop H264Encoder", e)
                Timber.e(e, "Failed to stop H264Encoder")
            }
            h264Encoder = null
        }
app/src/main/java/com/anyun/h264/model/WatermarkInfo.java
New file
@@ -0,0 +1,140 @@
package com.anyun.h264.model;
/**
 * 水印信息数据模型
 * 包含车牌、学员、教练、经度、纬度、驾校、车速等信息
 */
public class WatermarkInfo {
    /** 车牌号 */
    private String plateNumber;
    /** 学员姓名 */
    private String student;
    /** 教练姓名 */
    private String coach;
    /** 经度 */
    private Double longitude;
    /** 纬度 */
    private Double latitude;
    /** 驾校名称 */
    private String drivingSchool;
    /** 车速(单位:km/h) */
    private Double speed;
    public WatermarkInfo() {
    }
    public String getPlateNumber() {
        return plateNumber;
    }
    public void setPlateNumber(String plateNumber) {
        this.plateNumber = plateNumber;
    }
    public String getStudent() {
        return student;
    }
    public void setStudent(String student) {
        this.student = student;
    }
    public String getCoach() {
        return coach;
    }
    public void setCoach(String coach) {
        this.coach = coach;
    }
    public Double getLongitude() {
        return longitude;
    }
    public void setLongitude(Double longitude) {
        this.longitude = longitude;
    }
    public Double getLatitude() {
        return latitude;
    }
    public void setLatitude(Double latitude) {
        this.latitude = latitude;
    }
    public String getDrivingSchool() {
        return drivingSchool;
    }
    public void setDrivingSchool(String drivingSchool) {
        this.drivingSchool = drivingSchool;
    }
    public Double getSpeed() {
        return speed;
    }
    public void setSpeed(Double speed) {
        this.speed = speed;
    }
    /**
     * 格式化水印信息为显示文本
     * @return 格式化的水印文本
     */
    public String formatWatermarkText() {
        StringBuilder sb = new StringBuilder();
        if (plateNumber != null && !plateNumber.isEmpty()) {
            sb.append("车牌:").append(plateNumber);
        }
        if (student != null && !student.isEmpty()) {
            if (sb.length() > 0) sb.append(" ");
            sb.append("学员:").append(student);
        }
        if (coach != null && !coach.isEmpty()) {
            if (sb.length() > 0) sb.append(" ");
            sb.append("教练:").append(coach);
        }
        if (longitude != null && latitude != null) {
            if (sb.length() > 0) sb.append(" ");
            sb.append("位置:").append(String.format("%.6f,%.6f", latitude, longitude));
        }
        if (drivingSchool != null && !drivingSchool.isEmpty()) {
            if (sb.length() > 0) sb.append(" ");
            sb.append("驾校:").append(drivingSchool);
        }
        if (speed != null) {
            if (sb.length() > 0) sb.append(" ");
            sb.append("车速:").append(String.format("%.1f", speed)).append("km/h");
        }
        return sb.toString();
    }
    @Override
    public String toString() {
        return "WatermarkInfo{" +
                "plateNumber='" + plateNumber + '\'' +
                ", student='" + student + '\'' +
                ", coach='" + coach + '\'' +
                ", longitude=" + longitude +
                ", latitude=" + latitude +
                ", drivingSchool='" + drivingSchool + '\'' +
                ", speed=" + speed +
                '}';
    }
}
app/src/main/java/com/anyun/h264/service/H264EncodeServiceClient.java
@@ -40,6 +40,10 @@
 * // 获取资源列表
 * List<ResourceInfo> resources = client.getResourceList("240101000000", "240101235959");
 * 
 * // 设置水印信息
 * String watermarkJson = "{\"plateNumber\":\"京A12345\",\"student\":\"张三\",\"coach\":\"李四\",\"longitude\":116.397128,\"latitude\":39.916527,\"drivingSchool\":\"XX驾校\",\"speed\":60.5}";
 * client.setWatermarkInfo(watermarkJson);
 *
 * // 解绑服务
 * client.unbindService();
 * </pre>
@@ -202,5 +206,28 @@
            return null;
        }
    }
    /**
     * 设置水印信息
     * @param watermarkInfo JSON格式的水印信息字符串,包含:plateNumber(车牌)、student(学员)、coach(教练)、longitude(经度)、latitude(纬度)、drivingSchool(驾校)、speed(车速)
     *                      示例:{"plateNumber":"京A12345","student":"张三","coach":"李四","longitude":116.397128,"latitude":39.916527,"drivingSchool":"XX驾校","speed":60.5}
     *                      如果传入null或空字符串,将清除水印
     * @return true-成功,false-失败
     */
    public boolean setWatermarkInfo(String watermarkInfo) {
        if (!isServiceBound()) {
            Log.e(TAG, "Service is not bound");
            return false;
        }
        try {
            service.setWatermarkInfo(watermarkInfo);
            Log.d(TAG, "setWatermarkInfo called with: " + watermarkInfo);
            return true;
        } catch (RemoteException e) {
            Log.e(TAG, "Error calling setWatermarkInfo", e);
            return false;
        }
    }
}
gradle/libs.versions.toml
@@ -9,6 +9,7 @@
activityCompose = "1.8.0"
composeBom = "2024.04.01"
netty = "4.1.48.Final"
timber = "4.7.1"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -26,6 +27,7 @@
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
netty-all = { group = "io.netty", name = "netty-all", version.ref = "netty" }
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
usbcameralib/CMakeLists.txt
@@ -17,10 +17,11 @@
        # Provides a relative path to your source file(s).
        src/main/cpp/libyuv.c
        src/main/cpp/apptimer.cpp
        src/main/cpp/charencode.cpp
        src/main/cpp/watermark.cpp
        src/main/cpp/ImageProc.c
        src/main/cpp/charencode.c
        src/main/cpp/jpeg.c
        src/main/cpp/watermark.c
        src/main/cpp/SonixCamera/ROMData.c
        src/main/cpp/SonixCamera/SFData.c
        src/main/cpp/SonixCamera/SonixCamera.c
@@ -62,7 +63,9 @@
        jpeg
        turbojpeg
        ${log-lib}
        ${g-lib})
        ${g-lib}
        c++_shared
)
#target_link_libraries( # Specifies the target library.
#        usbcamera-lib
usbcameralib/build.gradle
@@ -14,7 +14,7 @@
        externalNativeBuild {
            cmake {
                arguments "-DANDROID_ARM_NEON=TRUE"
                arguments "-DANDROID_ARM_NEON=TRUE","-DANDROID_STL=c++_shared"
                cppFlags ""
            }
        }
@@ -43,6 +43,7 @@
        pickFirst 'lib/armeabi-v7a/libturbojpeg.so'
        pickFirst 'lib/arm64-v8a/libjpeg.so'
        pickFirst 'lib/arm64-v8a/libturbojpeg.so'
        pickFirst 'lib/*/libc++_shared.so'
    }
usbcameralib/src/main/cpp/ImageProc.c
@@ -1107,6 +1107,11 @@
        (*env)->ReleaseStringUTFChars(env, name, except_camera_name);
    }
    if (ret == SUCCESS_LOCAL) {
        InitWatermark("/system/ms_unicode_24.bin", IMG_WIDTH, IMG_HEIGHT);
        wm_enable = true;
    }
    return ret;
}
@@ -1122,6 +1127,9 @@
    pthread_mutex_lock(&mutex);
    cam_inited = false;
    wm_enable = false;
    UninitWatermark();
    LOGI("stopcapturing");
    stopcapturing();
usbcameralib/src/main/cpp/apptimer.cpp
New file
@@ -0,0 +1,262 @@
//
// Created by YY on 2021/3/16.
//
#include "apptimer.h"
//#include "../jni_log.h"
#include <cstdint>
#include <functional>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <list>
#include <thread>
#include <map>
#include <string>
#include <sstream>
#include <iomanip>
#define DEBUG(fmt, args...)     //LOGD("<apptimer> <%s>: " fmt, __func__, ##args)
std::mutex mtx;
std::map<void(*)(apptimer_var_t), CTimer *> TimerList;
static void RemoveTimerList(const CTimer *p);
CTimer::CTimer() {
    flag = 0;
    var.var_ptr = nullptr;
    var.var1 = var.var2 = var.var_length = 0;
    pthread = nullptr;
}
CTimer::~CTimer() {
    if (pthread != nullptr) {
        delete pthread;
    }
    if (var.var_ptr != nullptr) {
        delete var.var_ptr;
    }
}
bool CTimer::_updateStatus(CTimer *timer) {
    if (timer->flag == 0) {
        return false;
    } else {
        return true;    //cancel
    }
}
void CTimer::_do_task_func(uint32_t msec, CTimer *timer, std::function<void(apptimer_var_t)> callback) {
    std::unique_lock<std::mutex> lk(timer->cv_mtx);
    auto now = std::chrono::steady_clock::now();
    if (timer->cv.wait_until(lk, now + std::chrono::milliseconds(msec), std::bind(_updateStatus, timer)) == false) {
        RemoveTimerList(timer);             // 从列表中删除索引
        if (timer->flag == 0) {
            callback(timer->var);
        }
    } else {
//        LOGD("Cancel %d", static_cast<int>(timer->flag));
        RemoveTimerList(timer);
    }
    //delete timer;
    lk.unlock();
//    std::thread(clear, timer).detach();
    delete timer;
}
void CTimer::copy(int value1, int value2, const void *data, int length)
{
    this->var.var1 = value1;
    this->var.var2 = value2;
    this->var.var_length = length;
    if (length > 0 && data != nullptr) {
        this->var.var_ptr = new uint8_t[length];
        memcpy(this->var.var_ptr, data, length);
    } else {
        this->var.var_ptr = nullptr;
    }
}
void CTimer::clear(CTimer *timer)
{
    if (timer != nullptr) {
        delete timer;
    }
}
bool CTimer::start(uint32_t msec, std::function<void(apptimer_var_t)> callback) {
    if (pthread == nullptr) {
        try {
            pthread = new std::thread(_do_task_func, msec, this, callback);
            pthread->detach();
        } catch (std::exception &ex) {
            DEBUG("%s", ex.what());
            return false;
        }
//        this->callbackThread = std::thread(_do_task_func, msec, this, callback);
//        this->callbackThread.detach();
    }
    return true;
}
void CTimer::stop() {
    std::unique_lock<std::mutex> lk(this->cv_mtx);
    this->flag = 1;
    this->cv.notify_one();
}
void AppTimer_init(void)
{
    TimerList.clear();
}
void AppTimer_add(void(*cb)(apptimer_var_t), uint32_t msec)
{
    CTimer *cTimer = new CTimer();
    cTimer->copy(0, 0, nullptr, 0);
    std::lock_guard<std::mutex> lock(mtx);
    TimerList.insert(std::pair<void (*)(apptimer_var_t), CTimer *>(cb, cTimer));
    if (cTimer->start(msec, cb) == false) {
        auto it = TimerList.find(cb);
        if (it != TimerList.end()) {
            CTimer *ptr = it->second;
            TimerList.erase(it);
        }
        delete cTimer;
    }
}
void AppTimer_add(void(*cb)(apptimer_var_t), uint32_t msec, int value1, int value2)
{
    CTimer *cTimer = new CTimer();
    cTimer->copy(value1, value2, nullptr, 0);
    std::lock_guard<std::mutex> lock(mtx);
    TimerList.insert(std::pair<void (*)(apptimer_var_t), CTimer *>(cb, cTimer));
    if (cTimer->start(msec, cb) == false) {
        auto it = TimerList.find(cb);
        if (it != TimerList.end()) {
            CTimer *ptr = it->second;
            TimerList.erase(it);
        }
        delete cTimer;
    }
}
void AppTimer_add(void(*cb)(apptimer_var_t), uint32_t msec, void *data, int lenght)
{
    CTimer *cTimer = new CTimer();
    cTimer->copy(0, 0, data, lenght);
    std::lock_guard<std::mutex> lock(mtx);
    TimerList.insert(std::pair<void (*)(apptimer_var_t), CTimer *>(cb, cTimer));
    if (cTimer->start(msec, cb) == false) {
        auto it = TimerList.find(cb);
        if (it != TimerList.end()) {
            CTimer *ptr = it->second;
            TimerList.erase(it);
        }
        delete cTimer;
    }
}
void AppTimer_delete(void(*cb)(apptimer_var_t))
{
    std::lock_guard<std::mutex> lock(mtx);
    auto it = TimerList.find(cb);
    if (it != TimerList.end()) {
        CTimer *ptr = it->second;
        TimerList.erase(it);
        ptr->stop();
    }
}
static void RemoveTimerList(const CTimer *p)
{
    std::lock_guard<std::mutex> lock(mtx);
    for (auto it = TimerList.begin(); it != TimerList.end(); ++it) {
        CTimer *ptr = it->second;
        if (ptr == p) {
            TimerList.erase(it);
            break;
        }
    }
}
uint32_t AppTimer_GetTickCount(void)
{
    std::chrono::milliseconds as = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch());
    return as.count();
}
uint64_t AppTimer_GetGmtTickCount(void)
{
    std::chrono::milliseconds as = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
    return as.count();
}
std::string FormatTime(const char *fmt)
{
    auto now = std::chrono::system_clock::now();
    auto timet = std::chrono::system_clock::to_time_t(now);
    auto localTime = *std::localtime(&timet);
    std::stringstream ss;
    std::string str;
    ss << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S");
    str = ss.str();
    return str;
}
std::time_t MktimeString(const char *str, const char *fmt)
{
    struct std::tm dt;
    std::istringstream ss(str);
    ss >> std::get_time(&dt, fmt);
    std::stringstream out;
    out << std::put_time(&dt, "%Y-%m-%d %H:%M:%S");
//    DEBUG("%s, %ld", out.str().c_str(), std::mktime(&dt));
    time_t utc = std::mktime(&dt);
    return utc;
}
time_t TimeStamp(int year, int mon, int day, int hour, int min, int sec)
{
    // YYHHDDhhmmss(GMT+8)
    struct tm test_tm;
    struct timeval tv;
    struct timezone tz;
    gettimeofday(&tv,&tz);
    memset(&test_tm, 0, sizeof(test_tm));
    test_tm.tm_year = year - 1900;
    test_tm.tm_mon = mon - 1;
    test_tm.tm_mday = day;
    test_tm.tm_hour = hour;
    test_tm.tm_min = min;
    test_tm.tm_sec = sec;
    return mktime(&test_tm) - tz.tz_minuteswest*60;
}
usbcameralib/src/main/cpp/apptimer.h
New file
@@ -0,0 +1,69 @@
//
// Created by YY on 2021/3/16.
//
#ifndef NDKADV_APPTIMER_H
#define NDKADV_APPTIMER_H
#include <cstdint>
#include <functional>
#include <thread>
#include <chrono>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <string>
#include <ctime>
#define D_SEC(n)        ((n)*1000UL)
#define D_MIN(n)        ((n)*1000UL*60UL)
#define D_HOUR(n)        ((n)*1000UL*60UL*60UL)
typedef struct {
    int var1;
    int var2;
    void * var_ptr;
    int var_length;
} apptimer_var_t;
class CTimer {
public:
    CTimer();
    virtual ~CTimer();
    static bool _updateStatus(CTimer *timer);
    static void _do_task_func(uint32_t msec, CTimer *timer, std::function<void(apptimer_var_t)> callback);
    bool start(uint32_t msec, std::function<void(apptimer_var_t)> callback);
    void stop();
    void copy(int value1, int value2, const void *data, int length);
    static void clear(CTimer *timer);
private:
    std::mutex cv_mtx;
    std::condition_variable cv;
//    std::thread callbackThread;
    std::thread *pthread;
    apptimer_var_t var;
public:
    std::atomic_int flag;
};
void AppTimer_init(void);
void AppTimer_add(void(*cb)(apptimer_var_t), uint32_t msec);
void AppTimer_add(void(*cb)(apptimer_var_t), uint32_t msec, int value1, int value2);
void AppTimer_add(void(*cb)(apptimer_var_t), uint32_t msec, void *data, int lenght);
void AppTimer_delete(void (*cb) (apptimer_var_t));
uint32_t AppTimer_GetTickCount(void);
uint64_t AppTimer_GetGmtTickCount(void);
std::string FormatTime(const char *fmt = "%Y-%m-%d %H:%M:%S");
std::time_t MktimeString(const char *str, const char *fmt);
time_t TimeStamp(int year, int mon, int day, int hour, int min, int sec);
#endif //NDKADV_APPTIMER_H
usbcameralib/src/main/cpp/charencode.cpp
New file
@@ -0,0 +1,204 @@
//
// Created by YY on 2021/10/22.
//
#include <stdlib.h>
#include "charencode.h"
/*****************************************************************************
 * 将一个字符的Unicode(UCS-2和UCS-4)编码转换成UTF-8编码.
 *
 * 参数:
 *    unic     字符的Unicode编码值
 *    pOutput  指向输出的用于存储UTF8编码值的缓冲区的指针
 *    outsize  pOutput缓冲的大小
 *
 * 返回值:
 *    返回转换后的字符的UTF8编码所占的字节数, 如果出错则返回 0 .
 *
 * 注意:
 *     1. UTF8没有字节序问题, 但是Unicode有字节序要求;
 *        字节序分为大端(Big Endian)和小端(Little Endian)两种;
 *        在Intel处理器中采用小端法表示, 在此采用小端法表示. (低地址存低位)
 *     2. 请保证 pOutput 缓冲区有最少有 6 字节的空间大小!
 ****************************************************************************/
int enc_unicode_to_utf8_one(unsigned long unic, unsigned char *pOutput,
                            int outSize)
{
    if (pOutput == NULL || outSize < 6)
        return 0;
    if (unic <= 0x0000007F)
    {
        // * U-00000000 - U-0000007F:  0xxxxxxx
        *pOutput = (unic & 0x7F);
        return 1;
    }
    else if (unic >= 0x00000080 && unic <= 0x000007FF)
    {
        // * U-00000080 - U-000007FF:  110xxxxx 10xxxxxx
        *(pOutput + 1) = (unic & 0x3F) | 0x80;
        *pOutput = ((unic >> 6) & 0x1F) | 0xC0;
        return 2;
    }
    else if (unic >= 0x00000800 && unic <= 0x0000FFFF)
    {
        // * U-00000800 - U-0000FFFF:  1110xxxx 10xxxxxx 10xxxxxx
        *(pOutput + 2) = (unic & 0x3F) | 0x80;
        *(pOutput + 1) = ((unic >> 6) & 0x3F) | 0x80;
        *pOutput = ((unic >> 12) & 0x0F) | 0xE0;
        return 3;
    }
    else if (unic >= 0x00010000 && unic <= 0x001FFFFF)
    {
        // * U-00010000 - U-001FFFFF:  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
        *(pOutput + 3) = (unic & 0x3F) | 0x80;
        *(pOutput + 2) = ((unic >> 6) & 0x3F) | 0x80;
        *(pOutput + 1) = ((unic >> 12) & 0x3F) | 0x80;
        *pOutput = ((unic >> 18) & 0x07) | 0xF0;
        return 4;
    }
    else if (unic >= 0x00200000 && unic <= 0x03FFFFFF)
    {
        // * U-00200000 - U-03FFFFFF:  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
        *(pOutput + 4) = (unic & 0x3F) | 0x80;
        *(pOutput + 3) = ((unic >> 6) & 0x3F) | 0x80;
        *(pOutput + 2) = ((unic >> 12) & 0x3F) | 0x80;
        *(pOutput + 1) = ((unic >> 18) & 0x3F) | 0x80;
        *pOutput = ((unic >> 24) & 0x03) | 0xF8;
        return 5;
    }
    else if (unic >= 0x04000000 && unic <= 0x7FFFFFFF)
    {
        // * U-04000000 - U-7FFFFFFF:  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
        *(pOutput + 5) = (unic & 0x3F) | 0x80;
        *(pOutput + 4) = ((unic >> 6) & 0x3F) | 0x80;
        *(pOutput + 3) = ((unic >> 12) & 0x3F) | 0x80;
        *(pOutput + 2) = ((unic >> 18) & 0x3F) | 0x80;
        *(pOutput + 1) = ((unic >> 24) & 0x3F) | 0x80;
        *pOutput = ((unic >> 30) & 0x01) | 0xFC;
        return 6;
    }
    return 0;
}
/*****************************************************************************
 * 将一个字符的UTF8编码转换成Unicode(UCS-2和UCS-4)编码.
 *
 * 参数:
 *    pInput      指向输入缓冲区, 以UTF-8编码
 *    Unic        指向输出缓冲区, 其保存的数据即是Unicode编码值,
 *                类型为unsigned long .
 *
 * 返回值:
 *    成功则返回该字符的UTF8编码所占用的字节数; 失败则返回0.
 *
 * 注意:
 *     1. UTF8没有字节序问题, 但是Unicode有字节序要求;
 *        字节序分为大端(Big Endian)和小端(Little Endian)两种;
 *        在Intel处理器中采用小端法表示, 在此采用小端法表示. (低地址存低位)
 ****************************************************************************/
int enc_utf8_to_unicode_one(const unsigned char* pInput, unsigned long *Unic)
{
    if (pInput == NULL || Unic == NULL)
        return 0;
    // b1 表示UTF-8编码的pInput中的高字节, b2 表示次高字节, ...
    char b1, b2, b3, b4, b5, b6;
    *Unic = 0x0; // 把 *Unic 初始化为全零
    int utfbytes = enc_get_utf8_size(*pInput);
    unsigned char *pOutput = (unsigned char *)Unic;
    switch (utfbytes)
    {
        case 0:
            *pOutput = *pInput;
            utfbytes += 1;
            break;
        case 2:
            b1 = *pInput;
            b2 = *(pInput + 1);
            if ((b2 & 0xE0) != 0x80)
                return 0;
            *pOutput = (b1 << 6) + (b2 & 0x3F);
            *(pOutput + 1) = (b1 >> 2) & 0x07;
            break;
        case 3:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            if (((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80))
                return 0;
            *pOutput = (b2 << 6) + (b3 & 0x3F);
            *(pOutput + 1) = (b1 << 4) + ((b2 >> 2) & 0x0F);
            break;
        case 4:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            b4 = *(pInput + 3);
            if (((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80)
                || ((b4 & 0xC0) != 0x80))
                return 0;
            *pOutput = (b3 << 6) + (b4 & 0x3F);
            *(pOutput + 1) = (b2 << 4) + ((b3 >> 2) & 0x0F);
            *(pOutput + 2) = ((b1 << 2) & 0x1C) + ((b2 >> 4) & 0x03);
            break;
        case 5:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            b4 = *(pInput + 3);
            b5 = *(pInput + 4);
            if (((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80)
                || ((b4 & 0xC0) != 0x80) || ((b5 & 0xC0) != 0x80))
                return 0;
            *pOutput = (b4 << 6) + (b5 & 0x3F);
            *(pOutput + 1) = (b3 << 4) + ((b4 >> 2) & 0x0F);
            *(pOutput + 2) = (b2 << 2) + ((b3 >> 4) & 0x03);
            *(pOutput + 3) = (b1 << 6);
            break;
        case 6:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            b4 = *(pInput + 3);
            b5 = *(pInput + 4);
            b6 = *(pInput + 5);
            if (((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80)
                || ((b4 & 0xC0) != 0x80) || ((b5 & 0xC0) != 0x80)
                || ((b6 & 0xC0) != 0x80))
                return 0;
            *pOutput = (b5 << 6) + (b6 & 0x3F);
            *(pOutput + 1) = (b5 << 4) + ((b6 >> 2) & 0x0F);
            *(pOutput + 2) = (b3 << 2) + ((b4 >> 4) & 0x03);
            *(pOutput + 3) = ((b1 << 6) & 0x40) + (b2 & 0x3F);
            break;
        default:
            return 0;
            break;
    }
    return utfbytes;
}
int enc_get_utf8_size(const unsigned char pInput)
{
    unsigned char c = pInput;
    // 0xxxxxxx 返回0
    // 10xxxxxx 不存在
    // 110xxxxx 返回2
    // 1110xxxx 返回3
    // 11110xxx 返回4
    // 111110xx 返回5
    // 1111110x 返回6
    if(c< 0x80) return 0;
    if(c>=0x80 && c<0xC0) return -1;
    if(c>=0xC0 && c<0xE0) return 2;
    if(c>=0xE0 && c<0xF0) return 3;
    if(c>=0xF0 && c<0xF8) return 4;
    if(c>=0xF8 && c<0xFC) return 5;
    if(c>=0xFC) return 6;
}
usbcameralib/src/main/cpp/watermark.cpp
File was renamed from usbcameralib/src/main/cpp/watermark.c
@@ -12,6 +12,7 @@
#include "watermark.h"
#include "charencode.h"
#include "ImageProc.h"
#include "apptimer.h"
//static pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
typedef enum {
@@ -33,6 +34,22 @@
static int pic_width = 0, pic_height = 0;
static color_t wm_color;
static void PrintTime(apptimer_var_t val)
{
    text_t text;
    text.x = 10;
    text.y = 10;
    memset(text.text, 0, sizeof (text.text));
    strcpy(text.text, FormatTime().c_str());
    PrepareWatermark(RED, 24, 2, 1, &text);
    AppTimer_add(PrintTime, D_SEC(1));
}
void InitWatermark(const char *font, int width, int height)
{
    LOGI("InitWatermark");
@@ -48,6 +65,8 @@
    pic_width = width;
    pic_height = height;
    AppTimer_add(PrintTime, D_SEC(1));
}
void UninitWatermark(void)
@@ -58,6 +77,7 @@
        wm = NULL;
    }
    wm_num = 0;
    AppTimer_delete(PrintTime);
}
void PrepareWatermark(int color, int font_size, int multiple, int num, const text_t *texts)
@@ -75,7 +95,7 @@
    const int bytes_per_line = font_size / 8;
    wm_num = 0;
    wm_color = color;
    wm_color = static_cast<color_t>(color);
    //FILE *fp = fopen("/storage/self/primary/ms_unicode.bin", "rb");
    //FILE *fp = fopen("/system/ms_unicode.bin", "rb");
@@ -86,14 +106,14 @@
        uint8_t zm[64*64/8];        // 最大
        for (int m = 0; m < num; ++m) {
            char *str = texts[m].text;
            const char *str = texts[m].text;
            int screen_x = texts[m].x;
            int screen_y = texts[m].y;
            offset = 0;
            while (str != NULL && offset < strlen(str)) {
                int skip = enc_utf8_to_unicode_one(str + offset, &unicode);
                int skip = enc_utf8_to_unicode_one(reinterpret_cast<const unsigned char *>(str + offset), &unicode);
                int next_x = 0;
                if (skip == 0) {
usbcameralib/src/main/cpp/watermark.h
@@ -5,6 +5,10 @@
#ifndef FLOATWINDOWVEDIO_WATERMARK_H
#define FLOATWINDOWVEDIO_WATERMARK_H
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
    int x;
    int y;
@@ -19,4 +23,8 @@
void AddWatermark(uint8_t *mem);
#ifdef __cplusplus
}
#endif
#endif //FLOATWINDOWVEDIO_WATERMARK_H