package com.safeluck.floatwindow.manager; import android.content.Context; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaFormat; import android.media.MediaMuxer; import android.media.MediaRecorder; import android.text.TextUtils; import android.util.Log; import com.anyun.libusbcamera.UsbCamera; import com.anyun.libusbcamera.WatermarkParam; import com.safeluck.floatwindow.MediaArgu; import com.safeluck.floatwindow.ResponseVO; import com.safeluck.floatwindow.util.GlobalData; import com.safeluck.floatwindow.util.VideoFileUtils; import timber.log.Timber; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; /** * USB摄像头录像管理器 * 每分钟保存一个MP4文件 */ public class UsbCameraRecordManager { private static final String TAG = "UsbCameraRecordManager"; private static final int FRAME_RATE = 20; private static final int I_FRAME_INTERVAL = 1; // 1秒一个I帧 private static final long RECORD_INTERVAL_MS = 60 * 1000; // 60秒 // 音频参数 private static final int SAMPLE_RATE = 44100; // 采样率 private static final int CHANNEL_COUNT = 1; // 单声道 private static final int AUDIO_BIT_RATE = 64000; // 音频码率 private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_MONO; private Context context; private MediaArgu mediaArgu; private RecordCallback callback; // USB摄像头相关 private UsbCamera usbCamera; private RecordThread recordThread; private boolean isRunning = false; private boolean cameraExists = false; // 分辨率数组 [width, height] private int[] resolutionArr = new int[]{640, 480}; // 是否开启摄像头加密 private boolean ay_encrypt = false; // MediaCodec和MediaMuxer private MediaCodec videoEncoder; private MediaCodec audioEncoder; private AudioRecord audioRecord; private MediaMuxer mediaMuxer; private int videoTrackIndex = -1; private int audioTrackIndex = -1; private boolean muxerStarted = false; // 音频相关 private AudioThread audioThread; private int audioBufferSize; // 当前录像文件 private File currentVideoFile; private long currentFileStartTime; // 录制开始时间(纳秒),用于时间戳同步 private volatile long recordingStartTimeNs = 0; /** * 录像回调接口 */ public interface RecordCallback { void onResult(ResponseVO response); } public UsbCameraRecordManager(Context context) { this.context = context; } /** * 设置回调 */ public void setCallback(RecordCallback callback) { this.callback = callback; } /** * 开始录像 */ public void startRecord(MediaArgu media) { if (media == null) { notifyCallback(0, -1, "MediaArgu is null"); return; } this.mediaArgu = media; // 设置分辨率 if (media.getM_screen() != null) { resolutionArr[0] = media.getM_screen().getWidth(); resolutionArr[1] = media.getM_screen().getHeight(); Timber.d("设置分辨率: %dx%d", resolutionArr[0], resolutionArr[1]); } try { // 检查并打开USB摄像头 if (!openUsbCamera()) { cameraExists = false; notifyCallback(0, -1, "USB摄像头打开失败"); return; } setWaterMask(); cameraExists = true; Timber.d("USB摄像头打开成功"); // 启动录像线程 startRecordThread(); notifyCallback(0, 0, "录像已启动"); } catch (Exception e) { Timber.e(e, "Failed to start record"); notifyCallback(0, -3, "启动录像失败: " + e.getMessage()); } } WatermarkParam watermarkParam; ArrayList watermarkParamList = new ArrayList<>(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); int baseY = 20; int fontSize= 24; private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); private ScheduledFuture watermarkFuture; private void setWaterMask() { // 防止重复 schedule(startRecord 可能被多次调用) if (watermarkFuture != null && !watermarkFuture.isCancelled()) { return; } if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) { scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); } watermarkFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { if (!TextUtils.isEmpty(GlobalData.getInstance().getWaterMaskInfo())){ Log.i(TAG,"tieshuiin"); if (resolutionArr[0]==320&&resolutionArr[1]==240){ fontSize = 24; baseY = 2; }else if (resolutionArr[0]==640&&resolutionArr[1]==480){ fontSize = 32; baseY = 4; }else if (resolutionArr[0]==1280&&resolutionArr[1]==720){ fontSize = 48; baseY = 6; }else{ baseY = 2; fontSize = 24; } String school = GlobalData.getInstance().parseWaterMaskInfo("school", "无", GlobalData.ShareType.STRING); watermarkParam = new WatermarkParam(10,baseY,school); watermarkParamList.clear(); watermarkParamList.add(watermarkParam); String teacher = GlobalData.getInstance().parseWaterMaskInfo("teacher", "无", GlobalData.ShareType.STRING); String stu = GlobalData.getInstance().parseWaterMaskInfo("student", "无", GlobalData.ShareType.STRING); baseY = fontSize*11/10+baseY; watermarkParam = new WatermarkParam(10,baseY,"教练:"+teacher+" 学员:"+stu); watermarkParamList.add(watermarkParam); double speed = GlobalData.getInstance().parseWaterMaskInfo("speed", 0.0, GlobalData.ShareType.DOUBLE); String czh = GlobalData.getInstance().parseWaterMaskInfo("car_license", "无", GlobalData.ShareType.STRING) + GlobalData.getInstance().getCameraTag; baseY = fontSize*11/10+baseY; watermarkParam = new WatermarkParam(10,resolutionArr[1]-baseY,czh +" "+String.format("速度:%.1f",speed)); watermarkParamList.add(watermarkParam); double latitude = GlobalData.getInstance().parseWaterMaskInfo("latitude", 29.51228918, GlobalData.ShareType.DOUBLE); double longitude = GlobalData.getInstance().parseWaterMaskInfo("longitude", 106.45556208, GlobalData.ShareType.DOUBLE); // new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) baseY = fontSize*11/10+baseY; watermarkParam = new WatermarkParam(10,resolutionArr[1]-fontSize, String.format("%.6f %.6f", latitude, longitude)+" "+sdf.format(new Date())); watermarkParamList.add(watermarkParam); if (resolutionArr[0]==320&&resolutionArr[1]==240){ usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin"); usbCamera.setWatermark(3,fontSize,1,watermarkParamList); }else if (resolutionArr[0]==640&&resolutionArr[1]==480){ usbCamera.enableWatermark(true,"/system/ms_unicode_32.bin"); usbCamera.setWatermark(3,fontSize,1,watermarkParamList); }else if (resolutionArr[0]==1280&&resolutionArr[1]==720){ usbCamera.enableWatermark(true,"/system/ms_unicode_48.bin"); usbCamera.setWatermark(3,fontSize,1,watermarkParamList); }else{ usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin"); usbCamera.setWatermark(3,fontSize,1,watermarkParamList); } } },1,1, TimeUnit.SECONDS); } private void stopWaterMaskSchedule() { Timber.i("%s_stopWaterMaskSchedule", TAG); try { if (watermarkFuture != null) { watermarkFuture.cancel(true); watermarkFuture = null; } } catch (Throwable t) { Timber.w(t, "cancel watermarkFuture failed"); } try { if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) { scheduledExecutorService.shutdownNow(); } } catch (Throwable t) { Timber.w(t, "shutdown watermark scheduledExecutorService failed"); } finally { scheduledExecutorService = null; } } /** * 停止录像 */ public void stopRecord() { Timber.d("stopRecord called"); stopWaterMaskSchedule(); // 停止音频线程 if (audioThread != null) { audioThread.stopRecording(); try { audioThread.join(1000); } catch (InterruptedException e) { Timber.e(e, "Error stopping audio thread"); } audioThread = null; } stopRecordThread(); releaseResources(); if (usbCamera != null) { usbCamera.stopCamera(); } notifyCallback(0, 4, "录像已停止"); } /** * 打开USB摄像头 */ private boolean openUsbCamera() { try { if (usbCamera == null) { usbCamera = new UsbCamera(); } // 打开摄像头之前先调用setenv usbCamera.setenv(); // 使用 prepareCamera 方法;根据 MediaArgu.usbCameraId 选择具体摄像头 // usbCameraId: 1 -> P1(0), 2 -> P2(2), 其他 -> 让库自行在 {0,2} 里选择 int usbId = (mediaArgu != null) ? mediaArgu.getUsbCameraId() : 0; int[] cameraIds; if (usbId == 2) { cameraIds = new int[]{2}; } else if (usbId == 1) { cameraIds = new int[]{0}; } else { cameraIds = new int[]{0, 2}; } String cameraName = null; // 不指定特定名称 // 如果返回非0,代表打开失败,则先stopCamera再重试,最多3次 int ret = -1; for (int i = 0; i < 3; i++) { ret = usbCamera.prepareCamera(cameraIds, cameraName, resolutionArr, ay_encrypt); Timber.d("USB录像摄像头第%d次打开结果: %d, 分辨率: %dx%d", i + 1, ret, resolutionArr[0], resolutionArr[1]); if (ret == 0) { break; } // 打开失败则先关闭再重试 usbCamera.stopCamera(); } // 成功标准:prepareCamera 返回 0 return ret == 0; } catch (Exception e) { Timber.e(e, "打开USB摄像头异常"); return false; } } /** * 初始化编码器和Muxer */ private boolean initEncoderAndMuxer() { try { int width = resolutionArr[0]; int height = resolutionArr[1]; // 创建视频编码器 MediaFormat videoFormat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, width, height); videoFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible); videoFormat.setInteger(MediaFormat.KEY_BIT_RATE, width * height * 3); // 码率 videoFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); videoFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, I_FRAME_INTERVAL); videoEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); videoEncoder.configure(videoFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); videoEncoder.start(); // 创建音频编码器 MediaFormat audioFormat = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, CHANNEL_COUNT); audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, AUDIO_BIT_RATE); audioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); audioEncoder.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); audioEncoder.start(); // 初始化AudioRecord audioBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT); if (audioBufferSize <= 0) { audioBufferSize = SAMPLE_RATE * 2; // 默认缓冲区大小 } audioRecord = new AudioRecord( MediaRecorder.AudioSource.MIC, SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT, audioBufferSize * 2 ); if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { Timber.e("AudioRecord初始化失败"); return false; } // 创建新的视频文件 currentVideoFile = VideoFileUtils.getVideoFile(context, mediaArgu.getTfCardFlag()); if (currentVideoFile == null) { Timber.e("Failed to create video file"); return false; } mediaMuxer = new MediaMuxer(currentVideoFile.getAbsolutePath(), MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); videoTrackIndex = -1; audioTrackIndex = -1; muxerStarted = false; currentFileStartTime = System.currentTimeMillis(); Timber.d("编码器和Muxer初始化成功,文件: %s", currentVideoFile.getAbsolutePath()); return true; } catch (Exception e) { Timber.e(e, "初始化编码器和Muxer失败"); return false; } } /** * 释放资源 */ private void releaseResources() { if (audioRecord != null) { try { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { audioRecord.stop(); } audioRecord.release(); } catch (Exception e) { Timber.e(e, "Error releasing audio record"); } audioRecord = null; } if (audioEncoder != null) { try { audioEncoder.stop(); audioEncoder.release(); } catch (Exception e) { Timber.e(e, "Error releasing audio encoder"); } audioEncoder = null; } if (videoEncoder != null) { try { videoEncoder.stop(); videoEncoder.release(); } catch (Exception e) { Timber.e(e, "Error releasing video encoder"); } videoEncoder = null; } if (mediaMuxer != null) { try { if (muxerStarted) { mediaMuxer.stop(); } mediaMuxer.release(); } catch (Exception e) { Timber.e(e, "Error releasing media muxer"); } mediaMuxer = null; } muxerStarted = false; videoTrackIndex = -1; audioTrackIndex = -1; } /** * 启动录像线程 */ private void startRecordThread() { if (recordThread == null || !isRunning) { isRunning = true; recordThread = new RecordThread(); recordThread.start(); Timber.d("Record thread started"); } } /** * 停止录像线程 */ private void stopRecordThread() { isRunning = false; if (recordThread != null) { try { recordThread.join(2000); } catch (InterruptedException e) { Timber.e(e, "Error stopping record thread"); } recordThread = null; } Timber.d("Record thread stopped"); } /** * 录像线程 */ private class RecordThread extends Thread { @Override public void run() { super.run(); Timber.d("RecordThread started"); try { int width = resolutionArr[0]; int height = resolutionArr[1]; // 计算YUV420缓冲区大小 int bufferSize = width * height * 3 / 2; byte[] buffer = new byte[bufferSize]; // 初始化编码器和Muxer if (!initEncoderAndMuxer()) { notifyCallback(0, -1, "初始化编码器失败"); return; } // 启动音频录制 if (audioRecord != null) { audioRecord.startRecording(); Timber.d("音频录制已启动"); } // 注意:不要在这里主动检查音频编码器输出格式 // 因为 MediaCodec 的 getOutputFormat() 在编码器启动后可能返回 null // 应该等待 INFO_OUTPUT_FORMAT_CHANGED 事件 // 启动音频编码线程 audioThread = new AudioThread(); audioThread.start(); Timber.d("开始录像,分辨率: %dx%d", width, height); // 记录开始时间(纳秒,用于精确时间戳) recordingStartTimeNs = System.nanoTime(); long frameCount = 0; long lastFileChangeTime = System.currentTimeMillis(); // 循环处理摄像头数据 while (isRunning && cameraExists) { // 检查是否需要创建新文件(每分钟) long currentTime = System.currentTimeMillis(); if (currentTime - lastFileChangeTime >= RECORD_INTERVAL_MS) { // 停止音频线程 if (audioThread != null) { audioThread.stopRecording(); try { audioThread.join(1000); } catch (InterruptedException e) { Timber.e(e, "Error stopping audio thread"); } audioThread = null; } // 释放当前资源 releaseResources(); // 初始化新的编码器和Muxer if (!initEncoderAndMuxer()) { Timber.e("Failed to create new video file"); break; } // 重置开始时间 recordingStartTimeNs = System.nanoTime(); // 重新启动音频录制 if (audioRecord != null) { audioRecord.startRecording(); } audioThread = new AudioThread(); audioThread.start(); lastFileChangeTime = currentTime; frameCount = 0; // 通知文件创建 if (currentVideoFile != null) { notifyCallback(2, 0, currentVideoFile.getName()); } } // 处理摄像头数据 int processResult = usbCamera.processCamera(); if (processResult == -1) { Timber.w("processCamera返回-1,摄像头可能断开"); cameraExists = false; notifyCallback(0, -1, "USB摄像头断开"); break; } // 获取YUV数据 (参数0表示录像) usbCamera.rgba(2, buffer); // 编码并写入文件 if (videoEncoder != null && mediaMuxer != null) { // 计算实际经过的时间(微秒) long elapsedTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000; encodeFrame(buffer, elapsedTimeUs, width, height); frameCount++; } // 控制帧率,约20fps Thread.sleep(50); } } catch (Exception e) { Timber.e(e, "Error in record thread"); cameraExists = false; notifyCallback(0, -1, "录像线程异常: " + e.getMessage()); } finally { // 停止音频线程 if (audioThread != null) { audioThread.stopRecording(); try { audioThread.join(1000); } catch (InterruptedException e) { Timber.e(e, "Error stopping audio thread"); } audioThread = null; } releaseResources(); Timber.d("RecordThread ended"); } } /** * 编码一帧数据 * @param yuvData YUV数据 * @param presentationTimeUs 时间戳(微秒),基于实际开始时间 * @param width 宽度 * @param height 高度 */ private void encodeFrame(byte[] yuvData, long presentationTimeUs, int width, int height) { try { // 获取输入缓冲区 int inputBufferIndex = videoEncoder.dequeueInputBuffer(10000); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = videoEncoder.getInputBuffer(inputBufferIndex); if (inputBuffer != null) { inputBuffer.clear(); inputBuffer.put(yuvData); // 使用实际经过的时间作为时间戳 videoEncoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length, presentationTimeUs, 0); } } // 获取输出缓冲区 MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0); while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 输出格式改变,添加视频轨道 MediaFormat newFormat = videoEncoder.getOutputFormat(); synchronized (UsbCameraRecordManager.this) { // 检查 muxer 是否已启动,如果已启动则无法添加轨道 if (muxerStarted) { Timber.e("Muxer already started, cannot add video track"); break; } if (videoTrackIndex < 0) { videoTrackIndex = mediaMuxer.addTrack(newFormat); Timber.d("视频轨道已添加: %d", videoTrackIndex); checkAndStartMuxer(); } } outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0); continue; } else if (outputBufferIndex >= 0) { ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(outputBufferIndex); // 只有在 muxer 已启动且轨道索引有效时才写入数据 if (outputBuffer != null && muxerStarted && videoTrackIndex >= 0) { outputBuffer.position(bufferInfo.offset); outputBuffer.limit(bufferInfo.offset + bufferInfo.size); try { mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo); } catch (Exception e) { Timber.e(e, "Error writing video sample data"); } } videoEncoder.releaseOutputBuffer(outputBufferIndex, false); } outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0); } } catch (Exception e) { Timber.e(e, "Error encoding frame"); } } } /** * 检查并启动Muxer(当视频和音频轨道都准备好时) */ private synchronized void checkAndStartMuxer() { if (!muxerStarted && videoTrackIndex >= 0 && audioTrackIndex >= 0) { try { mediaMuxer.start(); muxerStarted = true; Timber.d("Muxer started, video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex); } catch (Exception e) { Timber.e(e, "Failed to start muxer"); } } else { Timber.d("Muxer not started yet, video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex); } } /** * 音频录制和编码线程 */ private class AudioThread extends Thread { private volatile boolean isRecording = true; public void stopRecording() { isRecording = false; } @Override public void run() { super.run(); Timber.d("AudioThread started"); try { byte[] audioBuffer = new byte[audioBufferSize]; long totalSamplesRead = 0; // 总采样数 while (isRecording && audioRecord != null && audioEncoder != null) { // 读取音频数据 int readSize = audioRecord.read(audioBuffer, 0, audioBuffer.length); if (readSize < 0) { Timber.w("AudioRecord read error: %d", readSize); break; } // 编码音频数据 encodeAudio(audioBuffer, readSize, totalSamplesRead); totalSamplesRead += readSize / 2; // 16位采样,每个采样2字节 } } catch (Exception e) { Timber.e(e, "Error in audio thread"); } finally { Timber.d("AudioThread ended"); } } /** * 编码音频数据 * @param audioData 音频数据 * @param size 数据大小(字节) * @param totalSamples 总采样数(从开始到现在的累计采样数) */ private void encodeAudio(byte[] audioData, int size, long totalSamples) { try { // 获取输入缓冲区(即使 muxer 未启动也要编码,以便尽快获得输出格式) int inputBufferIndex = audioEncoder.dequeueInputBuffer(10000); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = audioEncoder.getInputBuffer(inputBufferIndex); if (inputBuffer != null) { inputBuffer.clear(); inputBuffer.put(audioData, 0, size); // 使用采样数计算时间戳(微秒) // totalSamples 是采样数,SAMPLE_RATE 是每秒采样数 long presentationTimeUs = (totalSamples * 1000000) / SAMPLE_RATE; audioEncoder.queueInputBuffer(inputBufferIndex, 0, size, presentationTimeUs, 0); } } // 获取输出缓冲区 MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 输出格式改变,添加音频轨道 MediaFormat newFormat = audioEncoder.getOutputFormat(); synchronized (UsbCameraRecordManager.this) { // 检查 muxer 是否已启动,如果已启动则无法添加轨道 if (muxerStarted) { Timber.e("Muxer already started, cannot add audio track"); break; } if (audioTrackIndex < 0) { audioTrackIndex = mediaMuxer.addTrack(newFormat); Timber.d("音频轨道已添加: %d", audioTrackIndex); checkAndStartMuxer(); } } outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); continue; } else if (outputBufferIndex >= 0) { ByteBuffer outputBuffer = audioEncoder.getOutputBuffer(outputBufferIndex); // 只有在 muxer 已启动且轨道索引有效时才写入数据 if (outputBuffer != null && muxerStarted && audioTrackIndex >= 0) { outputBuffer.position(bufferInfo.offset); outputBuffer.limit(bufferInfo.offset + bufferInfo.size); try { mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo); } catch (Exception e) { Timber.e(e, "Error writing audio sample data"); } } audioEncoder.releaseOutputBuffer(outputBufferIndex, false); } outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); } } catch (Exception e) { Timber.e(e, "Error encoding audio"); } } } /** * 通知回调 */ private void notifyCallback(int type, int errCode, String message) { if (callback != null) { ResponseVO response = new ResponseVO(); response.setType(type); response.setErrCode(errCode); response.setMessage(message); callback.onResult(response); } } }