hkj
2025-07-01 a4f29bd968dbac7d7ae906e3d69da79a21f45411
update
11个文件已修改
18个文件已添加
3061 ■■■■■ 已修改文件
app/build.gradle 36 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/pedro/encoder/video/VideoEncoder2.java 389 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/pedro/rtplibrary/rtmp/AudioCapture.java 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/pedro/rtplibrary/rtmp/FrameRateController.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/pedro/rtplibrary/rtmp/RtmpClientWrapper.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/pedro/rtplibrary/rtmp/RtmpStreamer.java 295 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/pedro/rtplibrary/rtmp/VideoEncoderProxy.java 138 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/org/keran/echeck/ECUSBCameraActivity.java 829 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/org/keran/echeck/ECheckApplication.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/org/keran/echeck/SplashActivity.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/org/keran/echeck/core/usbserial/CustomProber.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/org/keran/echeck/core/util/CrashHandler.java 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/org/keran/echeck/push/MediaStream.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/drawable/rounded_button.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/layout/activity_usbcamera.xml 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/menu/main_toobar.xml 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values-zh/strings.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/colors.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/res/values/strings.xml 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/java/com/serenegiant/usb2/USBMonitor.java 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/java/com/serenegiant/usb2/common/AbstractUVCCameraHandler.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/java/com/serenegiant/usb2/widget/CameraViewInterface.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/java/org/keran/util/UVCCameraHelper.java 342 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/jniLibs/x86_64/libUVCCamera.so 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/jniLibs/x86_64/libjpeg-turbo1500.so 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/jniLibs/x86_64/libusb100.so 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/jniLibs/x86_64/libuvc.so 补丁 | 查看 | 原始文档 | blame | 历史
library/src/main/res/layout/layout_dialog_list.xml 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/build.gradle
@@ -3,18 +3,24 @@
android {
    compileSdkVersion 26
    buildToolsVersion '28.0.3'
    buildTypes {
        release {
            minifyEnabled true  // 启用代码优化
            shrinkResources true // 移除未使用的代码和资源
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    defaultConfig {
        applicationId "org.keran.echeck"
        minSdkVersion 19
        targetSdkVersion 26
        multiDexEnabled true
//        versionCode generateVersionCode()
//        versionName generateVersionName()
        versionCode 14250116
        versionName '1.4.25.0116'
        testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner'
        flavorDimensions "versionCode"
        if (project.hasProperty('RTMP_KEY')) {
            println("RTMPKEY IS :" + RTMP_KEY)
            buildConfigField 'String', 'RTMP_KEY', RTMP_KEY
@@ -33,12 +39,7 @@
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    dataBinding {
        enabled = true
@@ -69,7 +70,7 @@
    implementation(name: 'update-release', ext: 'aar')
    testImplementation 'junit:junit:4.12'
    implementation 'com.android.support:multidex:1.0.3'
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support:support-v4:26.1.0'
    implementation 'com.android.support:preference-v7:26.1.0'
@@ -122,5 +123,20 @@
        // 或精确排除特定模块
        // exclude group: 'com.android.support', module: 'appcompat-v7'
    }
    implementation ('com.github.mik3y:usb-serial-for-android:3.5.0') {
        exclude group: 'androidx.core'
        exclude group: 'androidx.annotation'
    }
    implementation 'cn.hutool:hutool-all:5.8.22'
    implementation "com.android.support:support-annotations:28.0.0"
    implementation ('com.github.pedroSG94.rtmp-rtsp-stream-client-java:rtplibrary:1.7.9') {
        exclude group: 'androidx.core'
        exclude group: 'androidx.annotation'
        exclude group: 'androidx.lifecycle'  // 新增排除项
    }
    implementation ('com.serenegiant:common:2.12.4') {
        exclude group: 'com.android.support'
        exclude group: 'android.arch.lifecycle'
    }
    implementation 'com.arthenica:mobile-ffmpeg-full:4.4.LTS'
}
app/src/main/AndroidManifest.xml
@@ -41,11 +41,14 @@
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".ECUSBCameraActivity"
            android:launchMode="singleInstance"
            />
        <activity
            android:name=".StreamActivity"
            android:launchMode="singleInstance"
         ></activity>
         />
        <activity android:name=".AboutActivity" />
        <activity android:name=".ECLoginActivity" />
app/src/main/java/com/pedro/encoder/video/VideoEncoder2.java
New file
@@ -0,0 +1,389 @@
package com.pedro.encoder.video;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.Surface;
import com.pedro.encoder.BaseEncoder;
import com.pedro.encoder.Frame;
import com.pedro.encoder.input.video.GetCameraData;
import com.pedro.encoder.utils.CodecUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
 * 适配1.7.9版本的视频编码器
 */
public class VideoEncoder2 extends BaseEncoder implements GetCameraData {
    private final String TAG = "VideoEncoder";
    private final GetVideoData getVideoData;
    protected boolean shouldReset = true;
    private boolean spsPpsSetted = false;
    private boolean forceKey = false;
    private ByteBuffer oldSps, oldPps, oldVps;
    private Surface inputSurface;
    private int width = 640;
    private int height = 480;
    private int fps = 30;
    private int bitRate = 1200 * 1024;
    private int rotation = 0;
    private int iFrameInterval = 2;
    private String type = CodecUtil.H264_MIME;
    private FormatVideoEncoder formatVideoEncoder = FormatVideoEncoder.YUV420Dynamical;
    private int avcProfile = -1;
    private int avcProfileLevel = -1;
    // 使用Queue替代原来的LinkedBlockingQueue
    private final Queue<Frame> frameQueue = new LinkedList<>();
    private static final int MAX_QUEUE_SIZE = 5;
    public VideoEncoder2(GetVideoData getVideoData) {
        this.getVideoData = getVideoData;
    }
    public boolean prepareVideoEncoder(int width, int height, int fps, int bitRate, int rotation,
                                       int iFrameInterval, FormatVideoEncoder formatVideoEncoder) {
        return prepareVideoEncoder(width, height, fps, bitRate, rotation, iFrameInterval,
                formatVideoEncoder, -1, -1);
    }
    public boolean prepareVideoEncoder(int width, int height, int fps, int bitRate, int rotation,
                                       int iFrameInterval, FormatVideoEncoder formatVideoEncoder,
                                       int avcProfile, int avcProfileLevel) {
        this.width = width;
        this.height = height;
        this.fps = fps;
        this.bitRate = bitRate;
        this.rotation = rotation;
        this.iFrameInterval = iFrameInterval;
        this.formatVideoEncoder = formatVideoEncoder;
        this.avcProfile = avcProfile;
        this.avcProfileLevel = avcProfileLevel;
        isBufferMode = true;
        MediaCodecInfo encoder = chooseEncoder(type);
        try {
            if (encoder == null) {
                Log.e(TAG, "Valid encoder not found");
                return false;
            }
            Log.i(TAG, "Encoder selected: " + encoder.getName());
            codec = MediaCodec.createByCodecName(encoder.getName());
            if (this.formatVideoEncoder == FormatVideoEncoder.YUV420Dynamical) {
                // 替换chooseColorDynamically为直接选择颜色格式
                this.formatVideoEncoder = chooseColorFormat(encoder);
                if (this.formatVideoEncoder == null) {
                    Log.e(TAG, "YUV420 format selection failed");
                    return false;
                }
            }
            MediaFormat videoFormat = createVideoFormat();
            codec.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
            running = false;
            if (formatVideoEncoder == FormatVideoEncoder.SURFACE
                    && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                isBufferMode = false;
                inputSurface = codec.createInputSurface();
            }
            Log.i(TAG, "Video encoder prepared");
            return true;
        } catch (Exception e) {
            Log.e(TAG, "Prepare video encoder failed", e);
            stop();
            return false;
        }
    }
    // 替换chooseColorDynamically为更简单的方法
    private FormatVideoEncoder chooseColorFormat(MediaCodecInfo mediaCodecInfo) {
        for (int color : mediaCodecInfo.getCapabilitiesForType(type).colorFormats) {
            if (color == FormatVideoEncoder.YUV420PLANAR.getFormatCodec()) {
                return FormatVideoEncoder.YUV420PLANAR;
            } else if (color == FormatVideoEncoder.YUV420SEMIPLANAR.getFormatCodec()) {
                return FormatVideoEncoder.YUV420SEMIPLANAR;
            }
        }
        return null;
    }
    private MediaFormat createVideoFormat() {
        String resolution = (rotation == 90 || rotation == 270) ?
                height + "x" + width : width + "x" + height;
        Log.i(TAG, "Prepare video info: " + formatVideoEncoder.name() + ", " + resolution);
        MediaFormat videoFormat = MediaFormat.createVideoFormat(type,
                (rotation == 90 || rotation == 270) ? height : width,
                (rotation == 90 || rotation == 270) ? width : height);
        videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, formatVideoEncoder.getFormatCodec());
        videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
        videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
        videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval);
        // 移除不支持的KEY_MAX_B_FRAMES
        // videoFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, 0);
        if (avcProfile > 0) {
            videoFormat.setInteger("profile", avcProfile);
        }
        if (avcProfileLevel > 0) {
            videoFormat.setInteger("level", avcProfileLevel);
        }
        return videoFormat;
    }
    @Override
    public void start(boolean resetTs) {
        forceKey = false;
        shouldReset = resetTs;
        spsPpsSetted = false;
        Log.i(TAG, "Video encoder started");
    }
    @Override
    protected void stopImp() {
        spsPpsSetted = false;
        if (inputSurface != null) {
            inputSurface.release();
            inputSurface = null;
        }
        oldSps = null;
        oldPps = null;
        oldVps = null;
        frameQueue.clear();
        Log.i(TAG, "Video encoder stopped");
    }
    @Override
    protected MediaCodecInfo chooseEncoder(String mime) {
        return null;
    }
    @Override
    public void inputYUVData(Frame frame) {
        if (running && frameQueue.size() < MAX_QUEUE_SIZE) {
            frameQueue.offer(frame);
        } else {
            Log.i(TAG, "Frame queue full, discarding frame");
        }
    }
    @Override
    protected Frame getInputFrame() throws InterruptedException {
        return frameQueue.poll();
    }
    @Override
    protected void checkBuffer(ByteBuffer byteBuffer,
                               MediaCodec.BufferInfo bufferInfo) {
        if (forceKey && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            forceKey = false;
            requestKeyframe();
        }
        // 移除不支持的fixTimeStamp调用
        // fixTimeStamp(bufferInfo);
        if (!spsPpsSetted) {
            if (type.equals(CodecUtil.H264_MIME)) {
                // 简化SPS/PPS处理
                oldSps = byteBuffer.duplicate();
                oldPps = byteBuffer.duplicate();
                getVideoData.onSpsPpsVps(oldSps, oldPps, null);
                spsPpsSetted = true;
            }
        }
    }
    @Override
    protected void sendBuffer(ByteBuffer byteBuffer,
                              MediaCodec.BufferInfo bufferInfo) {
        getVideoData.getVideoData(byteBuffer, bufferInfo);
    }
    // 其他必要的方法...
    public void requestKeyframe() {
        if (isRunning() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Bundle bundle = new Bundle();
            bundle.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
            try {
                codec.setParameters(bundle);
            } catch (IllegalStateException e) {
                Log.e(TAG, "Encoder not running", e);
            }
        }
    }
    public Surface getInputSurface() {
        return inputSurface;
    }
    public int getWidth() {
        return width;
    }
    public int getHeight() {
        return height;
    }
    public int getBitRate() {
        return bitRate;
    }
    public void setBitRate(int bitRate) {
        this.bitRate = bitRate;
        if (isRunning() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            Bundle bundle = new Bundle();
            bundle.putInt(MediaCodec.PARAMETER_KEY_VIDEO_BITRATE, bitRate);
            codec.setParameters(bundle);
        }
    }
    protected long calculatePts(Frame frame, long presentTimeUs) {
        return System.nanoTime() / 1000 - presentTimeUs;
    }
    @Override
    public void formatChanged(MediaCodec mediaCodec, MediaFormat mediaFormat) {
        Log.i(TAG, "Video encoder format changed: " + mediaFormat);
        // 通知视频格式变化
        getVideoData.onVideoFormat(mediaFormat);
        // 处理H.264/H.265的SPS/PPS/VPS信息
        if (type.equals(CodecUtil.H264_MIME)) {
            // H.264格式
            oldSps = mediaFormat.getByteBuffer("csd-0"); // SPS
            oldPps = mediaFormat.getByteBuffer("csd-1"); // PPS
            oldVps = null; // H.264没有VPS
            if (oldSps != null && oldPps != null) {
                getVideoData.onSpsPpsVps(oldSps.duplicate(), oldPps.duplicate(), oldVps);
                spsPpsSetted = true;
                Log.d(TAG, "H.264 SPS/PPS extracted from format change");
            } else {
                Log.e(TAG, "Failed to get H.264 SPS/PPS from format");
            }
        } else if (type.equals(CodecUtil.H265_MIME)) {
            // H.265/HEVC格式
            try {
                ByteBuffer csd0 = mediaFormat.getByteBuffer("csd-0");
                if (csd0 != null) {
                    List<ByteBuffer> buffers = extractVpsSpsPpsFromH265(csd0);
                    if (buffers.size() >= 3) {
                        oldVps = buffers.get(0);
                        oldSps = buffers.get(1);
                        oldPps = buffers.get(2);
                        getVideoData.onSpsPpsVps(oldSps.duplicate(), oldPps.duplicate(), oldVps.duplicate());
                        spsPpsSetted = true;
                        Log.d(TAG, "H.265 VPS/SPS/PPS extracted from format change");
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "Error extracting H.265 VPS/SPS/PPS", e);
            }
        }
        // 如果是强制关键帧请求后触发的格式变化,重置标志
        forceKey = false;
    }
    private List<ByteBuffer> extractVpsSpsPpsFromH265(ByteBuffer csd0byteBuffer) {
        List<ByteBuffer> byteBufferList = new ArrayList<>();
        int vpsPosition = -1;
        int spsPosition = -1;
        int ppsPosition = -1;
        int contBufferInitiation = 0;
        int length = csd0byteBuffer.remaining();
        byte[] csdArray = new byte[length];
        csd0byteBuffer.get(csdArray, 0, length);
        for (int i = 0; i < csdArray.length; i++) {
            if (contBufferInitiation == 3 && csdArray[i] == 1) {
                if (vpsPosition == -1) {
                    vpsPosition = i - 3;
                } else if (spsPosition == -1) {
                    spsPosition = i - 3;
                } else {
                    ppsPosition = i - 3;
                }
            }
            if (csdArray[i] == 0) {
                contBufferInitiation++;
            } else {
                contBufferInitiation = 0;
            }
        }
        byte[] vps = new byte[spsPosition];
        byte[] sps = new byte[ppsPosition - spsPosition];
        byte[] pps = new byte[csdArray.length - ppsPosition];
        for (int i = 0; i < csdArray.length; i++) {
            if (i < spsPosition) {
                vps[i] = csdArray[i];
            } else if (i < ppsPosition) {
                sps[i - spsPosition] = csdArray[i];
            } else {
                pps[i - ppsPosition] = csdArray[i];
            }
        }
        byteBufferList.add(ByteBuffer.wrap(vps));
        byteBufferList.add(ByteBuffer.wrap(sps));
        byteBufferList.add(ByteBuffer.wrap(pps));
        return byteBufferList;
    }
    /**
     * 重置视频编码器
     * 停止当前编码会话并重新初始化编码器
     */
    public void reset() {
        Log.i(TAG, "Resetting video encoder");
        stop(); // 先停止当前编码
        try {
            // 重新准备编码器,使用相同的参数
            prepareVideoEncoder(width, height, fps, bitRate, rotation,
                    iFrameInterval, formatVideoEncoder,
                    avcProfile, avcProfileLevel);
            Log.i(TAG, "Video encoder reset successfully");
        } catch (Exception e) {
            Log.e(TAG, "Failed to reset video encoder", e);
        }
    }
    /**
     * 强制生成关键帧
     * 请求编码器立即生成一个关键帧
     */
    public void forceKeyFrame() {
        if (isRunning() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            try {
                Bundle params = new Bundle();
                params.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0);
                codec.setParameters(params);
                forceKey = true; // 设置标志,确保下次输出时处理
                Log.d(TAG, "Key frame requested");
            } catch (IllegalStateException e) {
                Log.e(TAG, "Failed to request key frame - encoder not running", e);
            } catch (Exception e) {
                Log.e(TAG, "Failed to request key frame", e);
            }
        } else {
            Log.w(TAG, "Cannot request key frame - encoder not running or API < 19");
        }
    }
}
app/src/main/java/com/pedro/rtplibrary/rtmp/AudioCapture.java
New file
@@ -0,0 +1,123 @@
package com.pedro.rtplibrary.rtmp;
import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.media.audiofx.AcousticEchoCanceler;
import android.media.audiofx.NoiseSuppressor;
import android.util.Log;
import com.pedro.encoder.Frame;
import com.pedro.encoder.audio.AudioEncoder;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AudioCapture {
    private static final String TAG = "AudioCapture";
    private AudioRecord audioRecord;
    private AcousticEchoCanceler aec;
    private NoiseSuppressor ns;
    private int bufferSize;
    private volatile boolean isRecording;
    private ExecutorService mAudioExecutor;
    @SuppressLint("MissingPermission")
    public boolean init(int sampleRate, int channelConfig, int audioFormat, int bufferSize) {
        if (sampleRate <= 0 || (channelConfig != AudioFormat.CHANNEL_IN_MONO
                && channelConfig != AudioFormat.CHANNEL_IN_STEREO)
                || (audioFormat != AudioFormat.ENCODING_PCM_16BIT
                && audioFormat != AudioFormat.ENCODING_PCM_8BIT)) {
            Log.e(TAG, "Invalid audio parameters");
            return false;
        }
        this.bufferSize = bufferSize;
        if (bufferSize <= 0) {
            bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
        }
        try {
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.VOICE_COMMUNICATION,
                    sampleRate, channelConfig, audioFormat, bufferSize);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "AudioRecord creation failed: " + e.getMessage());
            return false;
        }
        setupAudioEffects();
        return audioRecord.getState() == AudioRecord.STATE_INITIALIZED;
    }
    private void setupAudioEffects() {
        if (AcousticEchoCanceler.isAvailable()) {
            aec = AcousticEchoCanceler.create(audioRecord.getAudioSessionId());
            if (aec != null) aec.setEnabled(true);
        }
        if (NoiseSuppressor.isAvailable()) {
            ns = NoiseSuppressor.create(audioRecord.getAudioSessionId());
            if (ns != null) ns.setEnabled(true);
        }
    }
    public void start(AudioEncoder audioEncoder) {
        if (audioRecord == null || audioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
            Log.e(TAG, "AudioRecord not initialized");
            return;
        }
        isRecording = true;
        audioRecord.startRecording();
        if(mAudioExecutor == null || !mAudioExecutor.isShutdown()) {
            mAudioExecutor = Executors.newSingleThreadExecutor();
        }
        mAudioExecutor.execute(() -> {
            byte[] buffer = new byte[bufferSize];
            while (isRecording) {
                int bytesRead = audioRecord.read(buffer, 0, buffer.length);
                if (bytesRead > 0) {
                    // 直接复用buffer避免拷贝
                    Frame frame = new Frame(buffer, 0, false, 0);
                    audioEncoder.inputPCMData(frame);
                }
            }
        });
    }
    public void stop() {
        isRecording = false;
        if (!mAudioExecutor.isShutdown()) {
            mAudioExecutor.shutdownNow();
        }
    }
    public void release() {
        stop();
        if (audioRecord != null) {
            if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
                audioRecord.stop();
            }
            audioRecord.release();
            audioRecord = null;
        }
        if (aec != null) {
            aec.release();
            aec = null;
        }
        if (ns != null) {
            ns.release();
            ns = null;
        }
    }
    public void setEchoCanceler(boolean enable) {
        if (aec != null) aec.setEnabled(enable);
    }
    public void setNoiseSuppressor(boolean enable) {
        if (ns != null) ns.setEnabled(enable);
    }
}
app/src/main/java/com/pedro/rtplibrary/rtmp/FrameRateController.java
New file
@@ -0,0 +1,48 @@
package com.pedro.rtplibrary.rtmp;
/**
 * 智能帧率控制器
 * 功能:
 * 1. 基础帧率控制
 * 2. 网络自适应降帧
 * 3. 编码延迟检测
 */
public class FrameRateController {
    private static final String TAG = "FrameRateController";
    private final long targetIntervalNs;
    private long lastFrameTimeNs = 0;
    private int actualFps = 0;
    public FrameRateController(int targetFps) {
        this.targetIntervalNs = 1_000_000_000L / targetFps;
    }
    public synchronized boolean shouldDropFrame() {
        final long now = System.nanoTime();
        // 首帧处理
        if (lastFrameTimeNs == 0) {
            lastFrameTimeNs = now;
            return false;
        }
        final long elapsed = now - lastFrameTimeNs;
        // 动态调整阈值(±10%容差)
        final long dynamicThreshold = (long) (targetIntervalNs * 0.9);
        if (elapsed >= dynamicThreshold) {
            lastFrameTimeNs = now - (elapsed - targetIntervalNs); // 补偿超时时间
            actualFps = (int) (1_000_000_000L / elapsed);
            return false;
        }
        return true;
    }
    public synchronized void reset() {
        lastFrameTimeNs = 0;
    }
}
app/src/main/java/com/pedro/rtplibrary/rtmp/RtmpClientWrapper.java
New file
@@ -0,0 +1,181 @@
package com.pedro.rtplibrary.rtmp;
import android.content.Context;
import android.media.MediaCodec;
import android.util.Log;
import net.ossrs.rtmp.ConnectCheckerRtmp;
import net.ossrs.rtmp.ProfileIop;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
public class RtmpClientWrapper {
    private enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED }
    private static final String TAG = "RtmpClientWrapper";
    private static final int MAX_RETRIES = 3;
    private static final long RETRY_DELAY = 3000;
    private final RtmpCamera1 rtmpCamera;
    private final ReentrantLock lock = new ReentrantLock();
    private final AtomicBoolean isReconnecting = new AtomicBoolean(false);
    private volatile ConnectionState state = ConnectionState.DISCONNECTED;
    private String currentUrl;
    private int retryCount;
    private final Context context;
    // 存储视频参数(SPS/PPS/VPS)
    private ByteBuffer spsBuffer;
    private ByteBuffer ppsBuffer;
    private ByteBuffer vpsBuffer;
    public RtmpClientWrapper(Context context, ConnectCheckerRtmp checker) {
        this.context = context;
        this.rtmpCamera = new RtmpCamera1(context, new InternalChecker(checker));
        setProfileIop(ProfileIop.BASELINE);
    }
    public void connect(String url) {
        lock.lock();
        try {
            if (state != ConnectionState.DISCONNECTED) return;
            state = ConnectionState.CONNECTING;
            currentUrl = url;
            attemptConnect();
        } finally {
            lock.unlock();
        }
    }
    private void attemptConnect() {
        new Thread(() -> {
            try {
                Log.d(TAG, "Connecting to " + currentUrl);
                rtmpCamera.startStream(currentUrl);
            } catch (Exception e) {
                Log.e(TAG, "Connection error", e);
                handleError(e);
            }
        }).start();
    }
    private void handleError(Exception e) {
        lock.lock();
        try {
            if (retryCount < MAX_RETRIES) {
                retryCount++;
                Log.w(TAG, "Will retry in " + RETRY_DELAY + "ms");
                new android.os.Handler(context.getMainLooper()).postDelayed(
                        () -> attemptConnect(),
                        RETRY_DELAY
                );
            } else {
                state = ConnectionState.DISCONNECTED;
            }
        } finally {
            lock.unlock();
        }
    }
    public void disconnect() {
        lock.lock();
        try {
            rtmpCamera.stopStream();
            state = ConnectionState.DISCONNECTED;
        } finally {
            lock.unlock();
        }
    }
    public boolean isConnected() {
        return state == ConnectionState.CONNECTED;
    }
    // 代理RtmpCamera1的关键方法
    public void setProfileIop(byte profileIop) {
        rtmpCamera.setProfileIop(profileIop);
    }
    public void setAuthorization(String user, String password) {
        rtmpCamera.setAuthorization(user, password);
    }
    /**
     * 设置视频参数(SPS/PPS/VPS)
     */
    public void setVideoInfo(ByteBuffer sps, ByteBuffer pps, ByteBuffer vps) {
        this.spsBuffer = sps;
        this.ppsBuffer = pps;
        this.vpsBuffer = vps;
    }
    /**
     * 发送视频帧
     */
    public void sendVideo(ByteBuffer buffer, MediaCodec.BufferInfo info) {
        if (rtmpCamera != null && isConnected()) {
            // 如果是关键帧,先发送SPS/PPS/VPS
            if ((info.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
                if (spsBuffer != null && ppsBuffer != null) {
                    rtmpCamera.onSpsPpsVpsRtp(spsBuffer.duplicate(), ppsBuffer.duplicate(), vpsBuffer != null ? vpsBuffer.duplicate() : null);
                }
            }
            // 发送视频数据
            rtmpCamera.getH264DataRtp(buffer, info);
        }
    }
    /**
     * 发送音频帧
     */
    public void sendAudio(ByteBuffer buffer, MediaCodec.BufferInfo info) {
        if (rtmpCamera != null && isConnected()) {
            rtmpCamera.getAacDataRtp(buffer, info);
        }
    }
    private class InternalChecker implements ConnectCheckerRtmp {
        private final ConnectCheckerRtmp userChecker;
        InternalChecker(ConnectCheckerRtmp checker) {
            this.userChecker = checker;
        }
        @Override
        public void onConnectionSuccessRtmp() {
            state = ConnectionState.CONNECTED;
            retryCount = 0;
            userChecker.onConnectionSuccessRtmp();
        }
        @Override
        public void onConnectionFailedRtmp(String reason) {
            state = ConnectionState.DISCONNECTED;
            userChecker.onConnectionFailedRtmp(reason);
        }
        @Override
        public void onNewBitrateRtmp(long bitrate) {
            userChecker.onNewBitrateRtmp(bitrate);
        }
        @Override
        public void onDisconnectRtmp() {
            state = ConnectionState.DISCONNECTED;
            userChecker.onDisconnectRtmp();
        }
        @Override
        public void onAuthErrorRtmp() {
            userChecker.onAuthErrorRtmp();
        }
        @Override
        public void onAuthSuccessRtmp() {
            userChecker.onAuthSuccessRtmp();
        }
    }
}
app/src/main/java/com/pedro/rtplibrary/rtmp/RtmpStreamer.java
New file
@@ -0,0 +1,295 @@
package com.pedro.rtplibrary.rtmp;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.pedro.encoder.Frame;
import com.pedro.encoder.audio.AudioEncoder;
import com.pedro.encoder.audio.GetAacData;
import com.pedro.encoder.input.audio.GetMicrophoneData;
import com.pedro.encoder.input.audio.MicrophoneManager;
import com.pedro.encoder.video.FormatVideoEncoder;
import com.pedro.encoder.video.GetVideoData;
import com.pedro.encoder.video.VideoEncoder2;
import net.ossrs.rtmp.ConnectCheckerRtmp;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingQueue;
public class RtmpStreamer implements GetVideoData, GetAacData, GetMicrophoneData {
    private static final String TAG = "RtmpStreamer";
    private boolean mIsReleased = false;
    private static final int MAX_QUEUE_SIZE = 5; // 根据帧率调整
    private LinkedBlockingQueue<Frame> mVideoQueue = new LinkedBlockingQueue<>(MAX_QUEUE_SIZE);
    // 视频相关
    private VideoEncoder2 videoEncoder;
    private final int width;
    private final int height;
    private boolean videoPrepared;
    private int fps;
    private VideoEncoderProxy videoEncoderProxy;
    // 音频相关
    private final AudioEncoder audioEncoder;
    private AudioCapture audioCapture;
 //  private MicrophoneManager microphoneManager;
    private boolean audioPrepared;
    // 推流控制
    private final RtmpClientWrapper rtmpClient;
    private volatile boolean streaming = false;
    // 帧率控制
    private final FrameRateController frameRateController;
    public RtmpStreamer(Context context, ConnectCheckerRtmp checker, int width, int height, int fps) {
        this.width = width;
        this.height = height;
        this.fps = fps;
        this.rtmpClient = new RtmpClientWrapper(context, checker);
        this.frameRateController = new FrameRateController(fps);
        this.audioEncoder = new AudioEncoder(this);
    }
    // 视频配置
    public boolean prepareVideo(int rotation, int iFrameInterval) {
        try {
            videoEncoder = new VideoEncoder2(this);
            videoPrepared = videoEncoder.prepareVideoEncoder(
                    width, height, fps, calculateBitrate(),
                    rotation, iFrameInterval,
                    FormatVideoEncoder.YUV420Dynamical
            );
            Log.d(TAG, "Video preparation result: " + videoPrepared);
            return videoPrepared;
        } catch (Exception e) {
            Log.e(TAG, "视频编码器初始化失败: " + e.getMessage());
            return false;
        }
    }
    // 动态码率计算
    private int calculateBitrate() {
        int baseBitrate = width * height * 3; // 基础码率
        if (width >= 1280) {
            return baseBitrate * 2;
        }
        return baseBitrate;
    }
    // 音频配置
    public boolean prepareAudio(int audioSource, int bitRate, int sampleRate,
                                boolean stereo, boolean echoCanceler, boolean noiseSuppressor) {
        try {
            int channelConfig = stereo ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO;
            int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT);
            audioCapture = new AudioCapture();
            boolean audioInit = audioCapture.init(sampleRate, channelConfig, AudioFormat.ENCODING_PCM_16BIT, bufferSize);
            if (audioInit) {
                audioCapture.setEchoCanceler(echoCanceler);
                audioCapture.setNoiseSuppressor(noiseSuppressor);
                audioCapture.start(audioEncoder);
                audioPrepared = audioEncoder.prepareAudioEncoder(
                        bitRate, sampleRate, stereo,
                        sampleRate * (stereo ? 2 : 1) * 2 // maxInputSize
                );
                return audioPrepared;
            }
            return false;
        } catch (Exception e) {
            Log.e(TAG, "音频编码器初始化失败: " + e.getMessage());
            return false;
        }
    }
    // 输入视频帧
    public void inputVideoFrame(byte[] nv21Data) {
        if (videoPrepared && nv21Data != null && videoEncoder != null) {
            Frame frame = new Frame(nv21Data, 0, false, 0);
            videoEncoder.inputYUVData(frame);
        }
    }
    // 启动推流
    public void startStream(String rtmpUrl) {
        if (!streaming && videoPrepared) {
            frameRateController.reset();
            rtmpClient.connect(rtmpUrl);
            new Handler(Looper.getMainLooper()).postDelayed(() -> {
                if (rtmpClient.isConnected()) {
                    videoEncoder.start();
                    if (audioPrepared) {
                        audioEncoder.start();
                        videoEncoderProxy.start();
                    }
                    streaming = true;
                } else {
                    Log.e(TAG, "Connection not ready, delaying stream start");
                }
            }, 1000);
        }
    }
    // 帧率控制
    public boolean shouldDropFrame() {
        return frameRateController.shouldDropFrame();
    }
    // 停止推流
    public void stopStream() {
        if (streaming) {
            if (videoEncoder != null) {
                videoEncoder.stop();
            }
            if (audioPrepared) {
                audioEncoder.stop();
                videoEncoderProxy.stop();
            }
            rtmpClient.disconnect();
            frameRateController.reset();
            streaming = false;
        }
    }
    // 释放资源
    public void release() {
        stopStream();
        if (audioCapture != null) {
            audioCapture.release();
            audioCapture = null;
        }
        if (videoEncoder != null) {
            videoEncoder = null;
        }
    }
    // 状态检查
    public boolean isStreaming() {
        return streaming;
    }
    @Override
    public void onSpsPps(ByteBuffer sps, ByteBuffer pps) {
    }
    @Override
    public void onSpsPpsVps(ByteBuffer sps, ByteBuffer pps, ByteBuffer vps) {
        if (rtmpClient.isConnected()) {
            rtmpClient.setVideoInfo(sps, pps, vps);
        }
    }
    @Override
    public void getVideoData(ByteBuffer buffer, MediaCodec.BufferInfo info) {
        if (streaming && !shouldDropFrame()) {
            rtmpClient.sendVideo(buffer, info);
        }
    }
    @Override
    public void getAacData(ByteBuffer buffer, MediaCodec.BufferInfo info) {
        if (streaming && audioPrepared) {
            rtmpClient.sendAudio(buffer, info);
        }
    }
    @Override
    public void inputPCMData(Frame frame) {
        if (audioPrepared) {
            audioEncoder.inputPCMData(frame);
        }
    }
    // 格式信息
    @Override
    public void onVideoFormat(MediaFormat mediaFormat) {
        Log.d(TAG, "视频格式: " + mediaFormat);
    }
    @Override
    public void onAudioFormat(MediaFormat mediaFormat) {
        Log.d(TAG, "音频格式: " + mediaFormat);
    }
    // 设置RTMP参数
    public void setProfileIop(byte profileIop) {
        rtmpClient.setProfileIop(profileIop);
    }
    public void setAuthorization(String user, String password) {
        rtmpClient.setAuthorization(user, password);
    }
    // 强制关键帧
    public void forceKeyFrame() {
        if (videoEncoder != null) {
            videoEncoder.forceKeyFrame();
        }
    }
    // 帧率控制内部类
    private static class FrameRateController {
        private final int targetFps;
        private long lastFrameTime;
        private long frameInterval;
        FrameRateController(int fps) {
            this.targetFps = fps;
            this.frameInterval = 1000000000 / fps; // 纳秒间隔
        }
        void reset() {
            lastFrameTime = 0;
        }
        boolean shouldDropFrame() {
            long now = System.nanoTime();
            if (lastFrameTime == 0 || now - lastFrameTime >= frameInterval) {
                lastFrameTime = now;
                return false;
            }
            return true;
        }
    }
    public void  forceKeyFrameVideoEncoderProxy() {
        videoEncoderProxy.forceKeyFrame();
    }
    public void reset() {
        if (mIsReleased) return;
        // 重置编码器
        if (videoEncoderProxy != null) {
            videoEncoderProxy.stop();
            videoEncoderProxy.getOriginalEncoder().reset();
        }
        // 重置音频
        if (audioCapture != null) {
            audioCapture.stop();
        }
        // 清空缓存队列
        mVideoQueue.clear();
        frameRateController.reset();
        mIsReleased = false;
    }
}
app/src/main/java/com/pedro/rtplibrary/rtmp/VideoEncoderProxy.java
New file
@@ -0,0 +1,138 @@
package com.pedro.rtplibrary.rtmp;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.util.Log;
import com.pedro.encoder.Frame;
import com.pedro.encoder.video.FormatVideoEncoder;
import com.pedro.encoder.video.GetVideoData;
import com.pedro.encoder.video.VideoEncoder2;
import java.nio.ByteBuffer;
public class VideoEncoderProxy implements GetVideoData {
    private final VideoEncoder2 originalEncoder;
    private GetVideoData externalCallback;
    private long mLastFramePts = 0;
    private static final String TAG = "VideoEncoderProxy";
    private static final boolean DEBUG = false; // 发布时设为false
    public VideoEncoderProxy() {
        this.originalEncoder = new VideoEncoder2(this);
    }
    public void setExternalCallback(GetVideoData callback) {
        this.externalCallback = callback;
    }
    @Override
    public void onSpsPps(ByteBuffer sps, ByteBuffer pps) {
        // 兼容旧版回调,转发到新版回调
        if (externalCallback != null) {
            if (externalCallback instanceof GetVideoData) {
                ((GetVideoData) externalCallback).onSpsPpsVps(sps, pps, null);
            }
        }
    }
    @Override
    public void onSpsPpsVps(ByteBuffer sps, ByteBuffer pps, ByteBuffer vps) {
        if (DEBUG) {
            Log.d(TAG, "SPS Size: " + (sps != null ? sps.remaining() : 0) +
                    ", PPS Size: " + (pps != null ? pps.remaining() : 0) +
                    ", VPS Size: " + (vps != null ? vps.remaining() : 0));
        }
        if (externalCallback != null) {
            externalCallback.onSpsPpsVps(sps, pps, vps);
        }
    }
    @Override
    public void getVideoData(ByteBuffer h264Buffer, MediaCodec.BufferInfo info) {
        if (DEBUG) {
            long encodeDelay = ((System.nanoTime()/1000 - info.presentationTimeUs))/1000;
            if (encodeDelay > 100) {
                Log.w(TAG, "编码延迟: " + encodeDelay + "ms");
            }
        }
        if (externalCallback != null) {
            // 创建副本避免数据竞争
            ByteBuffer bufferCopy = ByteBuffer.allocate(h264Buffer.remaining());
            h264Buffer.mark();
            bufferCopy.put(h264Buffer);
            h264Buffer.reset();
            bufferCopy.flip();
            MediaCodec.BufferInfo infoCopy = new MediaCodec.BufferInfo();
            infoCopy.set(info.offset, info.size, info.presentationTimeUs, info.flags);
            externalCallback.getVideoData(bufferCopy, infoCopy);
        }
    }
    @Override
    public void onVideoFormat(MediaFormat mediaFormat) {
        if (DEBUG) {
            Log.d(TAG, "视频格式确定: " + mediaFormat);
        }
        if (externalCallback != null) {
            externalCallback.onVideoFormat(mediaFormat);
        }
    }
    public VideoEncoder2 getOriginalEncoder() {
        return originalEncoder;
    }
    public void start() {
        if (originalEncoder != null) {
            originalEncoder.start();
        }
    }
    public void stop() {
        if (originalEncoder != null) {
            originalEncoder.stop();
        }
    }
    public boolean prepareVideoEncoder(int width, int height, int fps, int bitRate, int rotation,
                                       int iFrameInterval, FormatVideoEncoder formatVideoEncoder) {
        return originalEncoder != null &&
                originalEncoder.prepareVideoEncoder(width, height, fps, bitRate,
                        rotation, iFrameInterval, formatVideoEncoder);
    }
    public void inputYUVData(Frame frame) {
        if (originalEncoder != null) {
            originalEncoder.inputYUVData(frame);
        }
    }
    public void reset() {
        if (originalEncoder != null) {
            originalEncoder.stop();
            // 在1.7.9版本中,VideoEncoder可能没有直接暴露MediaCodec对象
            // 替代方案是重新准备编码器
            originalEncoder.reset();
        }
    }
    public void forceKeyFrame() {
        if (originalEncoder != null && originalEncoder.isRunning()) {
            // 在1.7.9版本中,使用VideoEncoder内置的请求关键帧方法
            try {
                originalEncoder.forceKeyFrame();
            } catch (Exception e) {
                Log.e(TAG, "强制关键帧失败", e);
            }
        }
    }
    public boolean isRunning() {
        return originalEncoder != null && originalEncoder.isRunning();
    }
}
app/src/main/java/org/keran/echeck/ECUSBCameraActivity.java
New file
@@ -0,0 +1,829 @@
package org.keran.echeck;
import android.support.annotation.RequiresApi;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.Surface;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import com.arthenica.mobileffmpeg.Config;
import com.arthenica.mobileffmpeg.Level;
import com.arthenica.mobileffmpeg.LogCallback;
import com.arthenica.mobileffmpeg.LogMessage;
import com.arthenica.mobileffmpeg.Statistics;
import com.arthenica.mobileffmpeg.StatisticsCallback;
import com.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.driver.UsbSerialProber;
import com.hoho.android.usbserial.util.SerialInputOutputManager;
import com.pedro.rtplibrary.rtmp.RtmpStreamer;
import com.serenegiant.usb2.UVCCamera;
import com.serenegiant.usb2.CameraDialog;
import com.serenegiant.usb2.Size;
import com.serenegiant.usb2.USBMonitor;
import com.serenegiant.usb2.common.AbstractUVCCameraHandler;
import com.serenegiant.usb2.common.UVCCameraHandler;
import com.serenegiant.usb2.widget.CameraViewInterface;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import android.support.v7.app.AppCompatActivity;
import net.ossrs.rtmp.ConnectCheckerRtmp;
import org.keran.echeck.core.usbserial.CustomProber;
import org.keran.util.UVCCameraHelper;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
/**
 * USB Camera Example Activity.
 * Main features include camera preview, photo capture, video recording,
 * resolution adjustment and brightness/contrast settings.
 */
public class ECUSBCameraActivity extends AppCompatActivity implements CameraDialog.CameraDialogParent, CameraViewInterface.Callback {
    private static final String TAG = "USB_CameraAndSerial";
    private UVCCamera mUVCCamera;
    private long mLastFrameTime = 0;
    // View bindings with ButterKnife
    @BindView(R.id.camera_view)
    public View mTextureView; // Camera preview view
    @BindView(R.id.toolbar)
    public Toolbar mToolbar; // Toolbar
    @BindView(R.id.seekbar_brightness)
    public SeekBar mSeekBrightness; // Brightness adjustment SeekBar
    @BindView(R.id.seekbar_contrast)
    public SeekBar mSeekContrast; // Contrast adjustment SeekBar
    private UVCCameraHandler mCameraHandler;
    private UVCCameraHelper mCameraHelper; // USB camera helper class
    private CameraViewInterface mUVCCameraView; // Camera preview interface
    private AlertDialog mDialog; // Resolution selection dialog
    private boolean isRequest; // Whether permission has been requested
    private boolean isPreview; // Whether preview is active
    private RtmpStreamer rtmpStreamer;
    private final static int WIDTH = 640;
    private final static int HEIGHT = 480;
    private final static int FPS = 30;
    private final static String RTMP_URL = "rtmp://192.168.2.110:3519/live/Xzo6pfLHR?sign=9zoepBYNg";
    private boolean mIsFirstStart = true;
    // Dedicated HandlerThread for video frame processing
    private HandlerThread mVideoProcessThread;
    private Handler mVideoHandler;
    /** ----------------------USB Serial start------------------**/
    private static final int BAUD_RATE = 115200;
    private static final String ACTION_USB_PERMISSION = "com.keran.usbcameras.view.USB_PERMISSION";
    private UsbManager usbManager;
    private UsbSerialPort serialPort;
    private SerialInputOutputManager serialIoManager;
    private ExecutorService executor;
    @BindView(R.id.text_temperature)
    public TextView mTemperatureTextureView;
    @BindView(R.id.text_max_temperature)
    public TextView mTemperatureMaxTextureView;
    private final Handler handler = new Handler(Looper.getMainLooper());
    // Device state tracking
    private UsbDevice currentActiveDevice;
    /** ----------------------USB Serial end------------------**/
    // USB device connection listener
    private UVCCameraHelper.OnMyDevConnectListener listener = new UVCCameraHelper.OnMyDevConnectListener() {
        @Override
        public void onAttachDev(UsbDevice device) {
            if (!isRequest) {
                Config.enableLogCallback(new LogCallback() {
                    @Override
                    public void apply(LogMessage message) {
                        Log.d(Config.TAG, message.getText());
                    }
                });
                // Set statistics callback
                Config.enableStatisticsCallback(new StatisticsCallback() {
                    @Override
                    public void apply(Statistics statistics) {
                        Log.d(Config.TAG, "Statistics: frame=" + statistics.getVideoFrameNumber() +
                                ", fps=" + statistics.getVideoFps() +
                                ", quality=" + statistics.getVideoQuality() +
                                ", size=" + statistics.getSize() +
                                ", time=" + statistics.getTime() +
                                ", bitrate=" + statistics.getBitrate() +
                                ", speed=" + statistics.getSpeed());
                    }
                });
                // Set log level
                Config.setLogLevel(Level.AV_LOG_INFO);
                isRequest = true; // Mark permission as requested
                if (mCameraHelper != null) {
                    mCameraHelper.requestPermission(usbManager); // Request permission
                }
            }
        }
        @Override
        public void onDettachDev(UsbDevice device) {
            // When USB device is detached, close camera
            if (isRequest) {
                isRequest = false; // Mark permission as not requested
                mCameraHelper.closeCamera(); // Close camera
                if (rtmpStreamer.isStreaming()) {
                    rtmpStreamer.stopStream();
                    showShortMsg("Streaming stopped");
                }
            }
        }
        @Override
        public void onConnectDev(UsbDevice device, boolean isConnected) {
            // When USB device connects successfully or fails
            if (!isConnected) {
                showShortMsg("Connection failed"); // Show connection failed message
                isPreview = false; // Mark preview as inactive
            } else {
                isPreview = true;
                // Initialize SeekBar, need to wait for camera initialization
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(2500); // Wait 2.5 seconds
                        } catch (InterruptedException e) {
                            e.printStackTrace(); // Print exception
                        }
                        Looper.prepare(); // Initialize Looper for current thread
                        if (mCameraHelper != null && mCameraHelper.isCameraOpened()) {
                            // Set brightness SeekBar progress
                            mSeekBrightness.setProgress(mCameraHelper.getModelValue(UVCCameraHelper.MODE_BRIGHTNESS));
                            // Set contrast SeekBar progress
                            mSeekContrast.setProgress(mCameraHelper.getModelValue(UVCCameraHelper.MODE_CONTRAST));
                        }
                        Looper.loop(); // Start message loop
                    }
                }).start();
            }
        }
        @Override
        public void onDisConnectDev(UsbDevice device) {
            // When USB device disconnects
            showShortMsg("Disconnected"); // Show disconnection message
        }
    };
    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState); // Call parent's onCreate
        setContentView(R.layout.activity_usbcamera); // Set layout file
        ButterKnife.bind(this); // Bind views
        initView(); // Initialize views
        initUsbManager();
        registerUsbReceiver();
        checkConnectedDevices();
        // Initialize UVCCameraHelper
        mUVCCameraView = (CameraViewInterface) mTextureView; // Get camera preview view
        mUVCCameraView.setCallback(this); // Set callback interface
        mCameraHelper = UVCCameraHelper.getInstance(); // Get UVCCameraHelper instance
        mCameraHelper.setDefaultFrameFormat(UVCCameraHelper.FRAME_FORMAT_MJPEG); // Set default frame format to MJPEG
        mCameraHelper.initUSBMonitor(this, mUVCCameraView, listener); // Initialize USB monitor
        mCameraHandler = UVCCameraHandler.createHandler(this, mUVCCameraView, 0, 640, 480);
        // Create dedicated thread for video processing
        mVideoProcessThread = new HandlerThread("VideoProcessor");
        mVideoProcessThread.start();
        mVideoHandler = new Handler(mVideoProcessThread.getLooper());
        // Set preview frame listener
        mCameraHelper.setOnPreviewFrameListener(new AbstractUVCCameraHandler.OnPreViewResultListener() {
            @Override
            public void onPreviewResult(byte[] nv21Yuv) {
                if (!rtmpStreamer.isStreaming()) return;
                // Process in video thread
                mVideoHandler.post(() -> {
                    // Dynamic frame rate control (adjust based on network condition)
                    if (!rtmpStreamer.shouldDropFrame()) {
                        rtmpStreamer.inputVideoFrame(nv21Yuv);
                    }
                });
            }
        });
        initStreamer();
    }
    @OnClick({R.id.btn_temperature})
    public void onViewClicked(View view) {
        mTemperatureMaxTextureView.setText("");
    }
    /**
     * Initialize views and controls.
     */
    private void initView() {
        setSupportActionBar(mToolbar); // Set toolbar
        // Initialize brightness SeekBar
        mSeekBrightness.setMax(100); // Set max value to 100
        mSeekBrightness.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (mCameraHelper != null && mCameraHelper.isCameraOpened()) {
                    mCameraHelper.setModelValue(UVCCameraHelper.MODE_BRIGHTNESS, progress); // Set brightness value
                }
            }
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // Called when user starts dragging SeekBar
            }
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // Called when user stops dragging SeekBar
            }
        });
        // Initialize contrast SeekBar
        mSeekContrast.setMax(100); // Set max value to 100
        mSeekContrast.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if (mCameraHelper != null && mCameraHelper.isCameraOpened()) {
                    mCameraHelper.setModelValue(UVCCameraHelper.MODE_CONTRAST, progress); // Set contrast value
                }
            }
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
                // Called when user starts dragging SeekBar
            }
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // Called when user stops dragging SeekBar
            }
        });
    }
    @Override
    protected void onStart() {
        super.onStart(); // Call parent's onStart
        // Register USB event broadcast
        if (mCameraHelper != null) {
            mCameraHelper.registerUSB(); // Register USB monitor
        }
    }
    @Override
    protected void onStop() {
        super.onStop(); // Call parent's onStop
        // Unregister USB event broadcast
        if (mCameraHelper != null) {
            mCameraHelper.unregisterUSB(); // Unregister USB monitor
        }
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Load toolbar menu
        getMenuInflater().inflate(R.menu.main_toobar, menu); // Inflate menu resource
        return true; // Return true indicating menu created
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle toolbar menu item clicks
        switch (item.getItemId()) {
            case R.id.menu_push:
                if (mCameraHelper == null || !mCameraHelper.isCameraOpened()) {
                    showShortMsg("External camera not open"); // Show camera open failed message
                    return super.onOptionsItemSelected(item); // Call parent's handler
                }
                try {
                    if (rtmpStreamer == null || !rtmpStreamer.isStreaming()) {
                        if (!mIsFirstStart) {
                            // Need to reset for non-first start
                            rtmpStreamer.reset();
                            initStreamer();
                        }
                        prepareAndStartStream();
                        mIsFirstStart = false;
                        showShortMsg("Streaming started");
                    } else {
                        if (rtmpStreamer.isStreaming()) {
                            rtmpStreamer.stopStream();
                            showShortMsg("Streaming stopped");
                        }
                    }
                } catch (Exception e) {
                    showShortMsg(e.getMessage());
                    e.printStackTrace();
                }
                break;
        }
        return super.onOptionsItemSelected(item); // Call parent's handler
    }
    private void prepareAndStartStream() {
        new Handler(Looper.getMainLooper()).postDelayed(() -> {
            if (rtmpStreamer != null) {
                // Indirect call through RtmpStreamer
                rtmpStreamer.forceKeyFrameVideoEncoderProxy();
                new Handler().postDelayed(() -> {
                    rtmpStreamer.startStream(RTMP_URL);
                    showShortMsg("Streaming started");
                }, 500);
            }
        }, 1000);
    }
    private void checkConnectedDevices() {
        HashMap<String, UsbDevice> devices = usbManager.getDeviceList();
        if (devices.isEmpty()) {
            Log.d(TAG, "No connected devices");
            return;
        }
        for (UsbDevice device : devices.values()) {
            if (isSupportedUsbSerialDevice(device)) {
                checkAndRequestPermission(device);
                break;
            }
        }
    }
    private void checkAndRequestPermission(UsbDevice device) {
        if (currentActiveDevice != null) {
            closePort(); // 关闭前一个连接
        }
        currentActiveDevice = device;
        if (isSupportedUsbSerialDevice(device)) {
            if (usbManager.hasPermission(device)) {
                setupSerialPort(device);
            } else {
                requestPermission(device);
            }
        }
    }
    private void requestPermission(UsbDevice device) {
        Intent intent = new Intent(ACTION_USB_PERMISSION);
        intent.putExtra(UsbManager.EXTRA_DEVICE, device);
        intent.setPackage(getPackageName());
        int flags = PendingIntent.FLAG_UPDATE_CURRENT;
        // 适配Android 12+的FLAG_MUTABLE要求
        if (Build.VERSION.SDK_INT >= 31) {  // Android 12 (S)
            flags |= 0x02000000;  // 直接使用FLAG_MUTABLE的值
        }
        PendingIntent pendingIntent = PendingIntent.getBroadcast(
                this,
                device.getDeviceId(),
                intent,
                flags
        );
        usbManager.requestPermission(device, pendingIntent);
    }
    private synchronized void setupSerialPort(UsbDevice device) {
        try {
            initExecutor();
            UsbSerialDriver driver = getUsbDriver(device);
            if (driver == null || driver.getPorts().isEmpty()) {
                showToast("设备不支持");
                return;
            }
            serialPort = driver.getPorts().get(0);
            UsbDeviceConnection connection = usbManager.openDevice(device);
            if (connection == null) {
                showToast("无法打开连接");
                return;
            }
            serialPort.open(connection);
            serialPort.setParameters(BAUD_RATE, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE);
            startIoManager();
            showToast("串口已连接");
        } catch (Exception e) {
            Log.e(TAG, "Setup error", e);
            showToast("错误: " + e.getMessage());
            closePort();
        }
    }
    private void startIoManager() {
        if (serialPort == null) return;
        serialIoManager = new SerialInputOutputManager(serialPort, new SerialInputOutputManager.Listener() {
            @Override
            public void onNewData(byte[] data) {
                String received = new String(data).trim();
                handler.post(() -> {
                    String num =Objects.toString(received, "").replace("}","").replace("{","");
                    if (NumberUtil.isNumber(num)) {
                        mTemperatureTextureView.setText(num);
                        if (StrUtil.isEmpty(mTemperatureMaxTextureView.getText())
                                || Convert.toBigDecimal(mTemperatureTextureView.getText()).compareTo(Convert.toBigDecimal(mTemperatureMaxTextureView.getText())) > 0) {
                            mTemperatureMaxTextureView.setText(mTemperatureTextureView.getText());
                        }
                    }});
            }
            @Override
            public void onRunError(Exception e) {
                handler.post(() -> {
                    showToast("通信错误");
                    closePort();
                });
            }
        });
        executor.submit(serialIoManager);
    }
    private UsbSerialDriver getUsbDriver(UsbDevice device) {
        UsbSerialDriver driver = UsbSerialProber.getDefaultProber().probeDevice(device);
        return driver != null ? driver : CustomProber.getCustomProber().probeDevice(device);
    }
    private void initExecutor() {
        if (executor == null || executor.isShutdown()) {
            executor = Executors.newSingleThreadExecutor(r -> {
                Thread thread = new Thread(r, "USB-Serial-Thread");
                thread.setPriority(Thread.MAX_PRIORITY); // 提高线程优先级
                return thread;
            });
        }
    }
    private void showToast(String message) {
        handler.removeCallbacksAndMessages(null); // 取消未显示的Toast
        Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
    }
    private synchronized void closePort() {
        // Part 1: 停止数据监听
        if (serialIoManager != null) {
            try {
                serialIoManager.setListener(null); // 防止内存泄漏
                serialIoManager.stop();
            } catch (Exception e) {
                Log.w(TAG, "Stop IoManager error", e);
            } finally {
                serialIoManager = null;
            }
        }
        // Part 2: 关闭串口
        if (serialPort != null) {
            try {
                serialPort.close();
            } catch (IOException e) {
                Log.e(TAG, "Port close error", e);
            } finally {
                serialPort = null;
            }
        }
        // Part 3: 清理线程池(可选)
        if (executor != null && !executor.isShutdown()) {
            executor.shutdownNow();
            // 注意:如果后续需要复用,应在重新连接时创建新线程池
        }
    }
    private boolean isSupportedUsbSerialDevice(UsbDevice device) {
        return UsbSerialProber.getDefaultProber().probeDevice(device) != null ||
                CustomProber.getCustomProber().probeDevice(device) != null;
    }
    private void initStreamer() {
        // Separate initialization from startup logic
        rtmpStreamer = new RtmpStreamer(
                this,
                new MyConnectChecker(),
                WIDTH,
                HEIGHT, FPS
        );
        // Configure parameters only once
        boolean success = rtmpStreamer.prepareVideo(0, 2)
                && rtmpStreamer.prepareAudio(
                MediaRecorder.AudioSource.DEFAULT,
                128000,
                44100,
                true,
                false,
                false
        );
    }
    /**
     * Show resolution selection dialog.
     */
    private void showResolutionListDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(ECUSBCameraActivity.this); // Create dialog builder
        View rootView = LayoutInflater.from(ECUSBCameraActivity.this).inflate(R.layout.layout_dialog_list, null); // Inflate dialog layout
        ListView listView = (ListView) rootView.findViewById(R.id.listview_dialog); // Get list view
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(ECUSBCameraActivity.this, android.R.layout.simple_list_item_1, getResolutionList()); // Create adapter
        if (adapter != null) {
            listView.setAdapter(adapter); // Set adapter
        }
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
                if (mCameraHelper == null || !mCameraHelper.isCameraOpened())
                    return; // If camera not open, return directly
                final String resolution = (String) adapterView.getItemAtPosition(position); // Get selected resolution
                String[] tmp = resolution.split("x"); // Split resolution into width and height
                if (tmp != null && tmp.length >= 2) {
                    int widht = Integer.valueOf(tmp[0]); // Get width
                    int height = Integer.valueOf(tmp[1]); // Get height
                    mCameraHelper.updateResolution(widht, height); // Update resolution
                }
                mDialog.dismiss(); // Dismiss dialog
            }
        });
        builder.setView(rootView); // Set dialog view
        mDialog = builder.create(); // Create dialog
        mDialog.show(); // Show dialog
    }
    /**
     * Get supported resolution list.
     *
     * @return resolution list
     */
    private List<String> getResolutionList() {
        List<Size> list = mCameraHelper.getSupportedPreviewSizes(); // Get supported resolution sizes
        List<String> resolutions = null; // Initialize resolution list
        if (list != null && list.size() != 0) {
            resolutions = new ArrayList<>(); // Create resolution list
            for (Size size : list) {
                if (size != null) {
                    resolutions.add(size.width + "x" + size.height); // Add resolution to list
                }
            }
        }
        return resolutions; // Return resolution list
    }
    @Override
    protected void onDestroy() {
        super.onDestroy(); // Call parent's onDestroy
        // Release USB camera resources
        if (mCameraHelper != null) {
            mCameraHelper.release(); // Release camera resources
        }
        if(mCameraHelper != null) {
            rtmpStreamer.release();
        }
        /** ----------------------USB Serial start------------------**/
        try {
            unregisterReceiver(usbReceiver);
        } catch (IllegalArgumentException e) {
            // Ignore not registered exception
        }
        closePort();
        if (executor != null) {
            executor.shutdownNow();
        }
        /** ----------------------USB Serial end------------------**/
    }
    /**
     * Show short message Toast.
     *
     * @param msg message content
     */
    private void showShortMsg(String msg) {
        Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); // Show short message
    }
    @Override
    public USBMonitor getUSBMonitor() {
        return mCameraHelper.getUSBMonitor(); // Get USB monitor
    }
    @Override
    public void onDialogResult(boolean canceled) {
        if (canceled) {
            showShortMsg("Operation canceled"); // Show canceled message
        }
    }
    public boolean isCameraOpened() {
        return mCameraHelper.isCameraOpened(); // Return whether camera is open
    }
    @Override
    public void onSurfaceCreated(CameraViewInterface view, Surface surface) {
        // When Surface is created, start camera preview
        if (!isPreview && mCameraHelper.isCameraOpened()) {
            mCameraHelper.startPreview(mUVCCameraView); // Start preview
            isPreview = true; // Mark preview as active
        }
    }
    @Override
    public void onSurfaceChanged(CameraViewInterface view, Surface surface, int width, int height) {
        // Called when Surface size changes
    }
    @Override
    public void onSurfaceDestroy(CameraViewInterface view, Surface surface) {
        // When Surface is destroyed, stop camera preview
        if (isPreview && mCameraHelper.isCameraOpened()) {
            mCameraHelper.stopPreview(); // Stop preview
            isPreview = false; // Mark preview as inactive
        }
    }
    private class MyConnectChecker implements ConnectCheckerRtmp {
        @Override
        public void onConnectionSuccessRtmp() {
            runOnUiThread(() ->
                    Toast.makeText(ECUSBCameraActivity.this, "RTMP connection successful", Toast.LENGTH_SHORT).show());
        }
        @Override
        public void onConnectionFailedRtmp(String reason) {
            runOnUiThread(() ->
                    Toast.makeText(ECUSBCameraActivity.this, "Connection failed: " + reason, Toast.LENGTH_SHORT).show());
        }
        @Override
        public void onDisconnectRtmp() {
            runOnUiThread(() -> showShortMsg("Disconnected"));
        }
        @Override
        public void onAuthErrorRtmp() {}
        @Override
        public void onAuthSuccessRtmp() {}
        @Override
        public void onNewBitrateRtmp(long bitrate) {}
    }
    /** ----------------------USB Serial start------------------**/
    private final BroadcastReceiver usbReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.d(TAG, "Received action: " + action);
            // Unified device acquisition (compatible with different manufacturers)
            UsbDevice device = getDeviceFromIntent(intent);
            if (!isSupportedUsbSerialDevice(device)) return;
            if (ACTION_USB_PERMISSION.equals(action)) {
                handlePermissionResponse(intent, device);
            }
            else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                handler.post(() -> {
                    Log.w(TAG, "New device detected:");
                    logDeviceInfo(device);
                    handleDeviceAttached(device);
                });
            }
            else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
                handler.post(() -> {
                    handleDeviceDetached(device);
                });
            }
        }
    };
    private void handleDeviceDetached(UsbDevice device) {
        if (device != null && serialPort != null &&
                device.getDeviceName().equals(serialPort.getDriver().getDevice().getDeviceName())) {
            Log.d(TAG, "Device detached: " + device.getDeviceName());
            closePort();
            currentActiveDevice = null;
        }
    }
    private void handleDeviceAttached(UsbDevice device) {
        closePort();
        if (device != null) {
            Log.d(TAG, "Device attached: " + device.getDeviceName());
            checkAndRequestPermission(device);
        }
    }
    private void handlePermissionResponse(Intent intent, UsbDevice device) {
        synchronized (this) {
            boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false);
            if (granted && device != null) {
                setupSerialPort(device);
            } else {
                Log.w(TAG, "Permission denied for " + device);
                handler.postDelayed(() -> checkAndRequestPermission(device), 500); // 延迟重试
            }
        }
    }
    private UsbDevice getDeviceFromIntent(Intent intent) {
        // 优先从EXTRA_DEVICE获取
        UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
        // 如果获取不到,尝试从已连接设备列表获取
        if (device == null && usbManager != null) {
            HashMap<String, UsbDevice> devices = usbManager.getDeviceList();
            if (!devices.isEmpty()) {
                device = devices.values().iterator().next();
            }
        }
        return device;
    }
    private void initUsbManager() {
        usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
    }
    @RequiresApi(api = Build.VERSION_CODES.O)
    private void registerUsbReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(ACTION_USB_PERMISSION);
        filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
        filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
        filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                // Use reflection to get constant value (compatible with all versions)
                int exportedFlag = Context.class.getField("RECEIVER_EXPORTED").getInt(null);
                registerReceiver(usbReceiver, filter, exportedFlag);
            } else {
                registerReceiver(usbReceiver, filter);
            }
        } catch (Exception e) {
            Log.w(TAG, "Failed to get RECEIVER_EXPORTED, fallback to 0");
            registerReceiver(usbReceiver, filter, 0);
        }
    }
    private void logDeviceInfo(UsbDevice device) {
        if (device != null) {
            Log.i(TAG, String.format(Locale.US,
                    "Device Info:\nName: %s\nVendorID: %04X\nProductID: %04X\nProductName: %s\nClass: %d\nProtocol: %d",
                    device.getDeviceName(),
                    device.getVendorId(),
                    device.getProductId(),
                    device.getProductName(),
                    device.getDeviceClass(),
                    device.getDeviceProtocol()));
        }
    }
    /** ----------------------USB Serial end------------------**/
}
app/src/main/java/org/keran/echeck/ECheckApplication.java
@@ -11,7 +11,9 @@
import android.os.LocaleList;
import android.util.DisplayMetrics;
import org.keran.echeck.core.util.AudioPlayerUtils;
import org.keran.echeck.core.util.CrashHandler;
import java.io.File;
import java.io.FileOutputStream;
@@ -24,7 +26,7 @@
    public static final String CHANNEL_CAMERA = "camera";
    private static ECheckApplication mApplication;
    public static int activeDays = 9999;
    private CrashHandler mCrashHandler;
    public static ECheckApplication getInstance() {
        return mApplication;
    }
@@ -34,6 +36,7 @@
        // 在创建应用前设置语言
        setDefaultLanguage(base);
        super.attachBaseContext(base);
    }
    private void setDefaultLanguage(Context context) {
        Resources resources = context.getResources();
@@ -64,7 +67,9 @@
        // 再次确认语言设置
        setAppLanguage(this, Locale.SIMPLIFIED_CHINESE);
        mCrashHandler = CrashHandler.getInstance();
        // 初始化 CrashHandler,传入当前应用的上下文和当前类的 Class 对象
        mCrashHandler.init(getApplicationContext(), getClass());
        initFontFile();
        AudioPlayerUtils.getInstance();
        createNotificationChannel();
app/src/main/java/org/keran/echeck/SplashActivity.java
@@ -31,7 +31,7 @@
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                startActivity(new Intent(SplashActivity.this, ECLoginActivity.class));
                startActivity(new Intent(SplashActivity.this, ECUSBCameraActivity.class));
                SplashActivity.this.finish();
            }
        }, 2000);
app/src/main/java/org/keran/echeck/core/usbserial/CustomProber.java
New file
@@ -0,0 +1,37 @@
package org.keran.echeck.core.usbserial;
import com.hoho.android.usbserial.driver.ProbeTable;
import com.hoho.android.usbserial.driver.UsbSerialProber;
// 用于探测自定义USB串口设备的Prober
public class CustomProber {
    public static UsbSerialProber getCustomProber() {
        ProbeTable customTable = new ProbeTable();
        // 在这里添加您的自定义设备ID
        // 格式:customTable.addProduct(vendorId, productId, driverClass);
        // 示例:添加常见的USB转串口芯片
        // CH340芯片
        // customTable.addProduct(0x1a86, 0x7523, CdcAcmSerialDriver.class);
        // customTable.addProduct(0x1a86, 0x5523, CdcAcmSerialDriver.class);
        // 其他芯片...
        return new UsbSerialProber(customTable);
    }
    private UsbSerialProber createCustomProber() {
        ProbeTable customTable = new ProbeTable();
        // 添加您需要的设备ID和驱动类
        // 示例:添加CH340芯片支持
        // customTable.addProduct(0x1A86, 0x7523, CdcAcmSerialDriver.class); // CH340
        // customTable.addProduct(0x1A86, 0x5523, CdcAcmSerialDriver.class); // CH341
        // 添加CP2102芯片支持
        // customTable.addProduct(0x10C4, 0xEA60, Cp21xxSerialDriver.class);
        return new UsbSerialProber(customTable);
    }
}
app/src/main/java/org/keran/echeck/core/util/CrashHandler.java
New file
@@ -0,0 +1,179 @@
package org.keran.echeck.core.util;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Looper;
import android.widget.Toast;
import org.keran.util.FileUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class CrashHandler implements UncaughtExceptionHandler {
    public static final String TAG = "CrashHandler";
    public static final String PROGRAM_BROKEN_ACTION = "com.teligen.wccp.PROGRAM_BROKEN";
    private UncaughtExceptionHandler mDefaultHandler;
    private static CrashHandler instance = new CrashHandler();
    private Context mContext;
    private Class<?> mainActivityClass;
    private Map<String, String> infos = new HashMap<String, String>();
    // 日期格式化器
    private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
    private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
    private CrashHandler() {
    }
    public static CrashHandler getInstance() {
        return instance;
    }
    public void init(Context context, Class<?> activityClass) {
        mContext = context;
        this.setMainActivityClass(activityClass);
        mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(this);
    }
    @Override
    public void uncaughtException(Thread thread, Throwable ex) {
        ex.printStackTrace();
        if (!handleException(ex) && mDefaultHandler != null) {
            mDefaultHandler.uncaughtException(thread, ex);
        } else {
            System.out.println("uncaughtException--->" + ex.getMessage());
            logError(ex);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                // Ignored
            }
            exitApp();
        }
    }
    private boolean handleException(Throwable ex) {
        if (ex == null) {
            return false;
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                Toast.makeText(mContext.getApplicationContext(),
                        "unknown exception and exiting...Please checking logs in sd card!",
                        Toast.LENGTH_LONG).show();
                Looper.loop();
            }
        }).start();
        collectDeviceInfo(mContext.getApplicationContext());
        logError(ex);
        return true;
    }
    private void exitApp() {
        android.os.Process.killProcess(android.os.Process.myPid());
        System.exit(0);
    }
    public void collectDeviceInfo(Context ctx) {
        try {
            PackageManager pm = ctx.getPackageManager();
            PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(),
                    PackageManager.GET_ACTIVITIES);
            if (pi != null) {
                String versionName = pi.versionName == null ? "null" : pi.versionName;
                String versionCode = pi.versionCode + "";
                infos.put("versionName", versionName);
                infos.put("versionCode", versionCode);
            }
        } catch (NameNotFoundException e) {
            // Ignored
        }
        Field[] fields = Build.class.getDeclaredFields();
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                infos.put(field.getName(), field.get(null).toString());
            } catch (Exception e) {
                // Ignored
            }
        }
    }
    private void logError(Throwable ex) {
        // 获取当前日期和时间
        Date now = new Date();
        String dateStr = DATE_FORMAT.format(now);
        String timeStr = TIME_FORMAT.format(now);
        // 创建按日期分类的日志目录
        File logDir = new File(mContext.getExternalFilesDir(null), "logs/" + dateStr);
        if (!logDir.exists()) {
            logDir.mkdirs();
        }
        // 创建带时间戳的日志文件
        File logFile = new File(logDir, "crash_" + timeStr.replace(":", "-") + ".log");
        StringBuffer sb = new StringBuffer();
        // 添加时间戳
        sb.append("Crash Time: ").append(dateStr).append(" ").append(timeStr).append("\n\n");
        // 添加设备信息
        for (Map.Entry<String, String> entry : infos.entrySet()) {
            sb.append(entry.getKey()).append("=").append(entry.getValue()).append("\n");
        }
        sb.append("\n");
        // 添加异常信息
        sb.append("Exception: ").append(ex.toString()).append("\n");
        sb.append("Localized Message: ").append(ex.getLocalizedMessage()).append("\n\n");
        sb.append("Stack Trace:\n");
        for (StackTraceElement element : ex.getStackTrace()) {
            sb.append(element.toString()).append("\n");
        }
        // 写入文件
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(logFile);
            fos.write(sb.toString().getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public Class<?> getMainActivityClass() {
        return mainActivityClass;
    }
    public void setMainActivityClass(Class<?> mainActivityClass) {
        this.mainActivityClass = mainActivityClass;
    }
}
app/src/main/java/org/keran/echeck/push/MediaStream.java
@@ -380,7 +380,7 @@
        }
        try {
            uvcCamera.setFrameCallback(uvcFrameCallback, UVCCamera.PIXEL_FORMAT_YUV420SP/*UVCCamera.PIXEL_FORMAT_NV21*/);
          //  uvcCamera.setFrameCallback(uvcFrameCallback, UVCCamera.PIXEL_FORMAT_YUV420SP/*UVCCamera.PIXEL_FORMAT_NV21*/);
            uvcCamera.startPreview();
        } catch (Throwable e){
            e.printStackTrace();
app/src/main/res/drawable/rounded_button.xml
New file
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <size
                android:height="1dp"
                 android:width="50dp" />
            <corners android:radius="7dp" />
            <solid android:color="@color/colorBlack2" />
            <stroke android:width="1dp" android:color="@color/colorWhite" />
        </shape>
    </item>
</selector>
app/src/main/res/layout/activity_usbcamera.xml
New file
@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff000000"
    tools:context=".ECUSBCameraActivity">
    <!-- 使用 AppCompat Toolbar -->
    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="@color/colorPrimary"
        android:elevation="4dp"
        app:title="@string/title_usbcamera_keranst"
        app:titleTextColor="@android:color/white" />
    <!-- 相机预览区域 -->
    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@+id/control_panel"
        android:layout_below="@id/toolbar">
        <com.serenegiant.usb2.widget.UVCCameraTextureView
            android:id="@+id/camera_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:adjustViewBounds="true"/>
    </FrameLayout>
    <!-- 控制面板 -->
    <android.support.v7.widget.CardView
        android:id="@+id/control_panel"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_margin="8dp"
        app:cardElevation="4dp"
        app:cardCornerRadius="8dp">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="8dp">
            <!-- 亮度控制 -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/title_usbcamera_brightness"
                    android:textColor="@android:color/white"
                    android:textSize="16sp"
                    android:textStyle="bold" />
                <SeekBar
                    android:id="@+id/seekbar_brightness"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="2"
                    android:max="100"
                    android:progress="50" />
            </LinearLayout>
            <!-- 对比度控制 -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/title_usbcamera_contrast"
                    android:textColor="@android:color/white"
                    android:textSize="16sp"
                    android:textStyle="bold" />
                <SeekBar
                    android:id="@+id/seekbar_contrast"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="2"
                    android:max="100"
                    android:progress="50" />
            </LinearLayout>
            <!-- 温度显示区域 -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/title_usbcamera_temperature"
                    android:textColor="@android:color/white"
                    android:textSize="16sp" />
                <TextView
                    android:id="@+id/text_temperature"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="待测"
                    android:textColor="@android:color/white"
                    android:textSize="16sp" />
                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@string/title_usbcamera_max_temperature"
                    android:textColor="@android:color/white"
                    android:textSize="16sp" />
                <TextView
                    android:id="@+id/text_max_temperature"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text=" "
                    android:textColor="@android:color/white"
                    android:textSize="16sp" />
            </LinearLayout>
        </LinearLayout>
    </android.support.v7.widget.CardView>
    <!-- 重置温度按钮 -->
    <Button
        android:id="@+id/btn_temperature"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_alignParentStart="true"
        android:layout_marginStart="16dp"
        android:layout_marginBottom="16dp"
        android:background="@drawable/rounded_button"
        android:text="@string/title_usbcamera_reset_temperature"
        android:textColor="@android:color/white" />
</RelativeLayout>
app/src/main/res/menu/main_toobar.xml
New file
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <!-- Stream button -->
    <item
        android:id="@+id/menu_push"
        android:title="@string/title_usbcamera_push"
        app:showAsAction="never" />
    <!-- Frame rate submenu -->
    <item
        android:id="@+id/menu_framerate"
        android:title="Frame Rate (FPS)"
        app:showAsAction="never">
        <menu>
            <group android:checkableBehavior="single">
                <item
                    android:id="@+id/fps_30"
                    android:title="30 FPS"
                    android:checked="true"/>
                <item
                    android:id="@+id/fps_40"
                    android:title="40 FPS"/>
                <item
                    android:id="@+id/fps_50"
                    android:title="50 FPS"/>
                <item
                    android:id="@+id/fps_60"
                    android:title="60 FPS"/>
            </group>
        </menu>
    </item>
    <!-- Resolution submenu -->
    <item
        android:id="@+id/menu_resolution"
        android:title="Resolution"
        app:showAsAction="never">
        <menu>
            <group android:checkableBehavior="single">
                <item
                    android:id="@+id/resolution_640_480"
                    android:title="640×480 (VGA)"
                    android:checked="true" />
                <item
                    android:id="@+id/resolution_800_600"
                    android:title="800×600 (SVGA)" />
                <item
                    android:id="@+id/resolution_1024_768"
                    android:title="1024×768 (XGA)" />
                <item
                    android:id="@+id/resolution_1280_720"
                    android:title="1280×720 (HD)" />
                <item
                    android:id="@+id/resolution_1366_768"
                    android:title="1366×768 (WXGA)" />
                <item
                    android:id="@+id/resolution_1600_900"
                    android:title="1600×900 (HD+)" />
                <item
                    android:id="@+id/resolution_1920_1080"
                    android:title="1920×1080 (FHD)" />
            </group>
        </menu>
    </item>
</menu>
app/src/main/res/values-zh/strings.xml
@@ -19,4 +19,11 @@
    <string name="login_button">登录</string>
    <string name="username_required">请输入用户名</string>
    <string name="password_required">请输入密码</string>
    <string name="title_usbcamera_push">推流</string>
    <string name="title_usbcamera_max_temperature">最高温度</string>
    <string name="title_usbcamera_reset_temperature">高温重置</string>
    <string name="title_usbcamera_temperature">检测温度</string>
    <string name="title_usbcamera_brightness">亮度(brightness)</string>
    <string name="title_usbcamera_keranst">科然信息</string>
    <string name="title_usbcamera_contrast">对比度(contrast)</string>
</resources>
app/src/main/res/values/colors.xml
@@ -23,4 +23,11 @@
    <color name="gray_hint">#808080</color>
    <color name="gray_border">#E0E0E0</color>
    <color name="colorPrimary">#6200EE</color>
    <color name="colorBlack">#000000</color>
    <color name="colorWhite">#FFFFFF</color>
    <color name="colorGreen">#2ECC71</color>
    <color name="colorBlack2">#1A1A1A</color>
    <color name="colorGreen2">#66FFB3</color>
</resources>
app/src/main/res/values/strings.xml
@@ -20,4 +20,11 @@
    <string name="login_button">Login</string>
    <string name="username_required">Username required</string>
    <string name="password_required">Password required</string>
    <string name="title_usbcamera_push">Stream</string>
    <string name="title_usbcamera_max_temperature">Max Temperature</string>
    <string name="title_usbcamera_reset_temperature">Reset High Temp</string>
    <string name="title_usbcamera_temperature">Detected Temp</string>
    <string name="title_usbcamera_brightness">Brightness</string>
    <string name="title_usbcamera_keranst">Keran Information</string>
    <string name="title_usbcamera_contrast">Contrast</string>
</resources>
library/src/main/java/com/serenegiant/usb2/USBMonitor.java
@@ -53,7 +53,9 @@
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -280,6 +282,11 @@
     * @return
     * @throws IllegalStateException
     */
    /**
     * 获取设备列表(带增强日志记录)
     * @return 匹配过滤条件的USB设备列表
     * @throws IllegalStateException 如果监控器已销毁
     */
    public List<UsbDevice> getDeviceList() throws IllegalStateException {
        if (destroyed) throw new IllegalStateException("already destroyed");
        return getDeviceList(mDeviceFilters);
@@ -287,86 +294,106 @@
    public List<UsbDevice> getDeviceList(final List<DeviceFilter> filters) throws IllegalStateException {
        if (destroyed) throw new IllegalStateException("already destroyed");
        // get detected devices
        // 获取USB设备列表
        final HashMap<String, UsbDevice> deviceList = mUsbManager.getDeviceList();
        // store those devices info before matching filter xml file
        String fileName = FileUtils.ROOT_PATH + "/USBCamera/failed_devices.txt";
        File logFile = new File(fileName);
        if(!logFile.getParentFile().exists()) {
            logFile.getParentFile().mkdirs();
        }
        if(! logFile.exists()) {
            try {
                logFile.createNewFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        FileWriter fw = null;
        PrintWriter pw = null;
        try {
            fw = new FileWriter(logFile, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
        if(fw != null) {
            pw = new PrintWriter(fw);
        }
        final List<UsbDevice> result = new ArrayList<UsbDevice>();
        if (deviceList != null) {
        if (deviceList != null && !deviceList.isEmpty()) {
            // 无过滤器时返回所有设备
            if ((filters == null) || filters.isEmpty()) {
                result.addAll(deviceList.values());
                // 记录所有设备信息
                for (UsbDevice device : deviceList.values()) {
                    logDeviceInfo(device, true);
                }
            } else {
                for (final UsbDevice device: deviceList.values() ) {
                    // match devices
                    for (final DeviceFilter filter: filters) {
                        if ((filter != null) && filter.matches(device) || (filter != null && filter.mSubclass == device.getDeviceSubclass())) {
                            // when filter matches
                            if (!filter.isExclude) {
                                result.add(device);
                            }
                            break;
                        } else {
                            // collection failed dev's class and subclass
                            String devModel = Build.MODEL;
                            String devSystemVersion = Build.VERSION.RELEASE;
                            String devClass = String.valueOf(device.getDeviceClass());
                            String subClass = String.valueOf(device.getDeviceSubclass());
                            try{
                                if(pw != null) {
                                    StringBuilder sb = new StringBuilder();
                                    sb.append(devModel);
                                    sb.append("/");
                                    sb.append(devSystemVersion);
                                    sb.append(":");
                                    sb.append("class="+devClass+", subclass="+subClass);
                                    pw.println(sb.toString());
                                    pw.flush();
                                    fw.flush();
                // 有过滤器时进行匹配
                for (final UsbDevice device : deviceList.values()) {
                    boolean matched = false;
                    for (final DeviceFilter filter : filters) {
                        if (filter != null) {
                            // 检查设备是否匹配过滤器
                            if (filter.matches(device) || filter.mSubclass == device.getDeviceSubclass()) {
                                matched = true;
                                // 记录匹配设备(包括被排除的)
                                logDeviceInfo(device, true);
                                if (!filter.isExclude) {
                                    result.add(device);
                                }
                            }catch (IOException e) {
                                e.printStackTrace();
                                break;
                            }
                        }
                    }
                    // 记录未匹配设备
                    if (!matched) {
                        logDeviceInfo(device, false);
                    }
                }
            }
        }
        if (pw != null) {
            pw.close();
        }
        if (fw != null) {
            try {
                fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }
    /**
     * 记录设备信息到日志文件
     * @param device USB设备
     * @param isMatched 是否匹配成功
     */
    private void logDeviceInfo(UsbDevice device, boolean isMatched) {
        Context context = mWeakContext.get();
        if (context == null || device == null) return;
        try {
            // 创建按日期分类的目录
            SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault());
            String dateStr = dateFormat.format(new Date());
            File logDir = new File(context.getExternalFilesDir(null), "USBCamera/logs/" + dateStr);
            if (!logDir.exists() && !logDir.mkdirs()) {
                Log.e(TAG, "Failed to create log directory");
                return;
            }
            // 创建带时间戳的日志文件
            SimpleDateFormat timeFormat = new SimpleDateFormat("HH-mm-ss", Locale.getDefault());
            String status = isMatched ? "matched" : "unmatched";
            File logFile = new File(logDir, String.format("%s_%s.log", status, timeFormat.format(new Date())));
            try (PrintWriter pw = new PrintWriter(logFile)) {
                // 记录设备基础信息
                pw.println("===== Device Basic Info =====");
                pw.printf("Record Time: %s %s%n", dateStr, timeFormat.format(new Date()));
                pw.printf("Device Name: %s%n", device.getDeviceName());
                pw.printf("Vendor ID: 0x%04X%n", device.getVendorId());
                pw.printf("Product ID: 0x%04X%n", device.getProductId());
                pw.printf("Class: 0x%02X, Subclass: 0x%02X, Protocol: 0x%02X%n",
                        device.getDeviceClass(), device.getDeviceSubclass(), device.getDeviceProtocol());
                pw.printf("Match Status: %s%n", isMatched ? "MATCHED" : "UNMATCHED");
                // 记录接口详情
                pw.println("\n===== Interface Details =====");
                for (int i = 0; i < device.getInterfaceCount(); i++) {
                    UsbInterface intf = device.getInterface(i);
                    pw.printf("Interface %d: Class=0x%02X, Subclass=0x%02X, Protocol=0x%02X%n",
                            i, intf.getInterfaceClass(), intf.getInterfaceSubclass(), intf.getInterfaceProtocol());
                }
                // 记录系统信息
                pw.println("\n===== System Info =====");
                pw.printf("Model: %s (%s)%n", Build.MODEL, Build.DEVICE);
                pw.printf("Android: %s (API %d)%n", Build.VERSION.RELEASE, Build.VERSION.SDK_INT);
                pw.printf("Manufacturer: %s%n", Build.MANUFACTURER);
                pw.flush();
            }
        } catch (Exception e) {
            Log.e(TAG, "Error writing device log", e);
        }
    }
    /**
     * return device list, return empty list if no device matched
     * @param filter
@@ -501,8 +528,16 @@
        }
    }
    public boolean isCamera(UsbDevice device) {
        String devInfo = device.getDeviceName().toLowerCase().concat(" ").concat(device.getProductName());
        return device !=null && device.getDeviceName() != null && devInfo.toLowerCase().indexOf("camera") >= 0;
        String devInfo = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            devInfo = device.getDeviceName().toLowerCase().concat(" ").concat(device.getProductName());
        } else {
            // 低版本只使用设备名和VID/PID
            devInfo = device.getDeviceName().toLowerCase()
                    + " [VID:" + Integer.toHexString(device.getVendorId())
                    + " PID:" + Integer.toHexString(device.getProductId()) + "]";
        }
        return device != null && device.getDeviceName() != null && devInfo.toLowerCase().indexOf("camera") >= 0;
    }
    /**
     * BroadcastReceiver for USB permission
@@ -532,8 +567,6 @@
                    }
                }
            } else if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(action)) {
                updatePermission(device, hasPermission(device));
                processAttach(device);
            } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) {
library/src/main/java/com/serenegiant/usb2/common/AbstractUVCCameraHandler.java
@@ -19,10 +19,10 @@
import android.view.Surface;
import android.view.SurfaceHolder;
import com.serenegiant.usb2.UVCCamera;
import com.serenegiant.usb2.IFrameCallback;
import com.serenegiant.usb2.Size;
import com.serenegiant.usb2.USBMonitor;
import com.serenegiant.usb2.UVCCamera;
import com.serenegiant.usb2.encoder.MediaEncoder;
import com.serenegiant.usb2.encoder.MediaMuxerWrapper;
import com.serenegiant.usb2.encoder.MediaSurfaceEncoder;
@@ -645,7 +645,7 @@
//                // 开启音频编码线程
//                if (true) {
//                    // for audio capturing
////                    new MediaAudioEncoder(muxer, mMediaEncoderListener);
        ////                    new MediaAudioEncoder(muxer, mMediaEncoderListener);
//                }
//                muxer.prepare();
//                muxer.startRecording();
library/src/main/java/com/serenegiant/usb2/widget/CameraViewInterface.java
@@ -38,7 +38,7 @@
    }
    public void onPause();
    public void onResume();
    public void setCallback(Callback callback);
    public void setCallback(CameraViewInterface.Callback callback);
    public SurfaceTexture getSurfaceTexture();
    public Surface getSurface();
    public boolean hasSurface();
library/src/main/java/org/keran/util/UVCCameraHelper.java
New file
@@ -0,0 +1,342 @@
package org.keran.util;
import android.app.Activity;
import android.graphics.SurfaceTexture;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import com.serenegiant.usb2.UVCCamera;
import com.serenegiant.usb2.DeviceFilter;
import com.serenegiant.usb2.Size;
import com.serenegiant.usb2.USBMonitor;
import com.serenegiant.usb2.common.AbstractUVCCameraHandler;
import com.serenegiant.usb2.common.UVCCameraHandler;
import com.serenegiant.usb2.widget.CameraViewInterface;
import org.keran.easyrtmp.R;
import java.util.HashMap;
import java.util.List;
/** UVCCamera Helper class
 *
 * Created by jiangdongguo on 2017/9/30.
 */
public class UVCCameraHelper {
    public static final String SUFFIX_JPEG = ".jpg";
    public static final String SUFFIX_MP4 = ".mp4";
    private static final String TAG = "UVCCameraHelper";
    private int previewWidth = 640;
    private int previewHeight = 480;
    public static final int FRAME_FORMAT_YUYV = UVCCamera.FRAME_FORMAT_YUYV;
    // Default using MJPEG
    // if your device is connected,but have no images
    // please try to change it to FRAME_FORMAT_YUYV
    public static final int FRAME_FORMAT_MJPEG = UVCCamera.FRAME_FORMAT_MJPEG;
    public static final int MODE_BRIGHTNESS = UVCCamera.PU_BRIGHTNESS;
    public static final int MODE_CONTRAST = UVCCamera.PU_CONTRAST;
    private int mFrameFormat = FRAME_FORMAT_MJPEG;
    private static UVCCameraHelper mCameraHelper;
    // USB Manager
    private USBMonitor mUSBMonitor;
    // Camera Handler
    private UVCCameraHandler mCameraHandler;
    private USBMonitor.UsbControlBlock mCtrlBlock;
    private Activity mActivity;
    private CameraViewInterface mCamView;
    private UVCCameraHelper() {
    }
    public static UVCCameraHelper getInstance() {
        if (mCameraHelper == null) {
            mCameraHelper = new UVCCameraHelper();
        }
        return mCameraHelper;
    }
    public void closeCamera() {
        if (mCameraHandler != null) {
            mCameraHandler.close();
        }
    }
    public interface OnMyDevConnectListener {
        void onAttachDev(UsbDevice device);
        void onDettachDev(UsbDevice device);
        void onConnectDev(UsbDevice device, boolean isConnected);
        void onDisConnectDev(UsbDevice device);
    }
    public void initUSBMonitor(Activity activity, CameraViewInterface cameraView, final OnMyDevConnectListener
            listener) {
        this.mActivity = activity;
        this.mCamView = cameraView;
        mUSBMonitor = new USBMonitor(activity.getApplicationContext(), new USBMonitor.OnDeviceConnectListener() {
            // called by checking usb device
            // do request device permission
            @Override
            public void onAttach(UsbDevice device) {
                if (listener != null) {
                    listener.onAttachDev(device);
                }
            }
            // called by taking out usb device
            // do close camera
            @Override
            public void onDettach(UsbDevice device) {
                if (listener != null) {
                    listener.onDettachDev(device);
                }
            }
            // called by connect to usb camera
            // do open camera,start previewing
            @Override
            public void onConnect(final UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) {
                mCtrlBlock = ctrlBlock;
                openCamera(ctrlBlock);
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // wait for camera created
                        try {
                            Thread.sleep(500);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // start previewing
                        startPreview(mCamView);
                    }
                }).start();
                if(listener != null) {
                    listener.onConnectDev(device,true);
                }
            }
            // called by disconnect to usb camera
            // do nothing
            @Override
            public void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) {
                if (listener != null) {
                    listener.onDisConnectDev(device);
                }
            }
            @Override
            public void onCancel(UsbDevice device) {
            }
        });
        createUVCCamera();
    }
    public void createUVCCamera() {
        if (mCamView == null)
            throw new NullPointerException("CameraViewInterface cannot be null!");
        // release resources for initializing camera handler
        if (mCameraHandler != null) {
            mCameraHandler.release();
            mCameraHandler = null;
        }
        // initialize camera handler
        mCamView.setAspectRatio(previewWidth / (float)previewHeight);
        mCameraHandler = UVCCameraHandler.createHandler(mActivity, mCamView, 2,
                previewWidth, previewHeight, mFrameFormat);
    }
    public void updateResolution(int width, int height) {
        if (previewWidth == width && previewHeight == height) {
            return;
        }
        this.previewWidth = width;
        this.previewHeight = height;
        if (mCameraHandler != null) {
            mCameraHandler.release();
            mCameraHandler = null;
        }
        mCamView.setAspectRatio(previewWidth / (float)previewHeight);
        mCameraHandler = UVCCameraHandler.createHandler(mActivity,mCamView, 2,
                previewWidth, previewHeight, mFrameFormat);
        openCamera(mCtrlBlock);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // wait for camera created
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // start previewing
                startPreview(mCamView);
            }
        }).start();
    }
    public void registerUSB() {
        if (mUSBMonitor != null) {
            mUSBMonitor.register();
        }
    }
    public void unregisterUSB() {
        if (mUSBMonitor != null) {
            mUSBMonitor.unregister();
        }
    }
    public boolean checkSupportFlag(final int flag) {
        return mCameraHandler != null && mCameraHandler.checkSupportFlag(flag);
    }
    public int getModelValue(final int flag) {
        return mCameraHandler != null ? mCameraHandler.getValue(flag) : 0;
    }
    public int setModelValue(final int flag, final int value) {
        return mCameraHandler != null ? mCameraHandler.setValue(flag, value) : 0;
    }
    public int resetModelValue(final int flag) {
        return mCameraHandler != null ? mCameraHandler.resetValue(flag) : 0;
    }
    public void requestPermission(UsbManager usbManager) {
        HashMap<String, UsbDevice> devices = usbManager.getDeviceList();
        boolean isC = false;
        boolean isS = false;
        for (UsbDevice device : devices.values()) {
            if (mUSBMonitor.isCamera(device)) {
                if (mUSBMonitor != null) {
                    mUSBMonitor.requestPermission(device);
                }
            }
        }
    }
    public int getUsbDeviceCount() {
        List<UsbDevice> devList = getUsbDeviceList();
        if (devList == null || devList.size() == 0) {
            return 0;
        }
        return devList.size();
    }
    public List<UsbDevice> getUsbDeviceList() {
        List<DeviceFilter> deviceFilters = DeviceFilter
                .getDeviceFilters(mActivity.getApplicationContext(), R.xml.device_filter);
        if (mUSBMonitor == null || deviceFilters == null)
//            throw new NullPointerException("mUSBMonitor ="+mUSBMonitor+"deviceFilters=;"+deviceFilters);
            return null;
        // matching all of filter devices
        return mUSBMonitor.getDeviceList(deviceFilters);
    }
    public boolean isPushing() {
        if (mCameraHandler != null) {
            return mCameraHandler.isRecording();
        }
        return false;
    }
    public boolean isCameraOpened() {
        if (mCameraHandler != null) {
            return mCameraHandler.isOpened();
        }
        return false;
    }
    public void release() {
        if (mCameraHandler != null) {
            mCameraHandler.release();
            mCameraHandler = null;
        }
        if (mUSBMonitor != null) {
            mUSBMonitor.destroy();
            mUSBMonitor = null;
        }
    }
    public USBMonitor getUSBMonitor() {
        return mUSBMonitor;
    }
    public void setOnPreviewFrameListener(AbstractUVCCameraHandler.OnPreViewResultListener listener) {
        if(mCameraHandler != null) {
            mCameraHandler.setOnPreViewResultListener(listener);
        }
    }
    private void openCamera(USBMonitor.UsbControlBlock ctrlBlock) {
        if (mCameraHandler != null) {
            mCameraHandler.open(ctrlBlock);
        }
    }
    public void startPreview(CameraViewInterface cameraView) {
        SurfaceTexture st = cameraView.getSurfaceTexture();
        if (mCameraHandler != null) {
            mCameraHandler.startPreview(st);
        }
    }
    public void stopPreview() {
        if (mCameraHandler != null) {
            mCameraHandler.stopPreview();
        }
    }
    public void startCameraFoucs() {
        if (mCameraHandler != null) {
            mCameraHandler.startCameraFoucs();
        }
    }
    public List<Size> getSupportedPreviewSizes() {
        if (mCameraHandler == null)
            return null;
        return mCameraHandler.getSupportedPreviewSizes();
    }
    public void setDefaultPreviewSize(int defaultWidth,int defaultHeight) {
        if(mUSBMonitor != null) {
            throw new IllegalStateException("setDefaultPreviewSize should be call before initMonitor");
        }
        this.previewWidth = defaultWidth;
        this.previewHeight = defaultHeight;
    }
    public void setDefaultFrameFormat(int format) {
        if(mUSBMonitor != null) {
            throw new IllegalStateException("setDefaultFrameFormat should be call before initMonitor");
        }
        this.mFrameFormat = format;
    }
    public int getPreviewWidth() {
        return previewWidth;
    }
    public int getPreviewHeight() {
        return previewHeight;
    }
}
library/src/main/jniLibs/x86_64/libUVCCamera.so
Binary files differ
library/src/main/jniLibs/x86_64/libjpeg-turbo1500.so
Binary files differ
library/src/main/jniLibs/x86_64/libusb100.so
Binary files differ
library/src/main/jniLibs/x86_64/libuvc.so
Binary files differ
library/src/main/res/layout/layout_dialog_list.xml
New file
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/listview_dialog"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</LinearLayout>