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.soBinary files differ
library/src/main/jniLibs/x86_64/libjpeg-turbo1500.soBinary files differ
library/src/main/jniLibs/x86_64/libusb100.soBinary files differ
library/src/main/jniLibs/x86_64/libuvc.soBinary 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>