| .gitignore | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/.gitignore | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/AndroidManifest.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/safeluck/floatwindow/FloatingService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/safeluck/floatwindow/MainActivity.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/safeluck/floatwindow/MediaArgu.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraPushManager.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraRecordManager.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/safeluck/floatwindow/util/AudioRecordManager.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| app/src/main/java/com/safeluck/floatwindow/util/GlobalData.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| readMe.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| usbcameralib/src/main/cpp/watermark.cpp | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
.gitignore
@@ -13,3 +13,52 @@ .externalNativeBuild .cxx local.properties /usbcameralib/.cxx/ /.kotlin/ # Build directories build/ **/build/ **/.cxx/ **/.kotlin/ **/.gradle/ **/build/ **/intermediates/ **/generated/ **/outputs/ **/.externalNativeBuild/ # Android Studio *.iml .idea/ .gradle/ local.properties *.apk *.ap_ *.aab # NDK obj/ .externalNativeBuild/ .cxx/ # Kotlin .kotlin/ # Generated files *.class *.dex *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar # Log files *.log # OS files .DS_Store Thumbs.db app/.gitignore
@@ -1 +1,2 @@ /build /build /build/ app/src/main/AndroidManifest.xml
@@ -34,6 +34,12 @@ android:name=".FloatingService" android:enabled="true" android:exported="false" /> <service android:name=".P2UsbCameraVideoService" android:enabled="true" android:exported="false" android:process=":p2" /> </application> </manifest> app/src/main/java/com/safeluck/floatwindow/FloatingService.java
@@ -1,7 +1,9 @@ package com.safeluck.floatwindow; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; import android.content.Intent; import android.os.IBinder; import android.os.RemoteCallbackList; @@ -32,6 +34,50 @@ // 当前使用的管理器类型 private ManagerType currentManagerType = ManagerType.NONE; // P2 跨进程服务(用于 usbCameraId == 2) private IMediaAidlInterface p2Service; private boolean p2Bound = false; private MediaArgu pendingP2StartMedia; private final IMyCallback p2Callback = new IMyCallback.Stub() { @Override public void onResult(ResponseVO re) throws RemoteException { // 将 P2 进程回调转发给客户端 notifyCallback(re); } }; private final ServiceConnection p2Connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { p2Service = IMediaAidlInterface.Stub.asInterface(service); p2Bound = true; Timber.d("P2UsbCameraVideoService connected"); try { p2Service.registerCallback(p2Callback); } catch (RemoteException e) { Timber.e(e, "Failed to register p2Callback"); } // 如果有 pending startMedia,连接后立刻执行 if (pendingP2StartMedia != null) { try { p2Service.startMedia(pendingP2StartMedia); pendingP2StartMedia = null; } catch (RemoteException e) { Timber.e(e, "Failed to startMedia on P2 service"); } } } @Override public void onServiceDisconnected(ComponentName name) { Timber.w("P2UsbCameraVideoService disconnected"); p2Bound = false; p2Service = null; } }; /** * 管理器类型枚举 @@ -40,7 +86,9 @@ NONE, USB_PUSH, USB_RECORD, ANDROID_RECORD ANDROID_RECORD, P2_USB_PUSH, P2_USB_RECORD } // AIDL Binder @@ -116,6 +164,16 @@ Timber.d("FloatingService onCreate"); } private void ensureP2Bound() { if (p2Bound) return; Intent intent = new Intent(this, P2UsbCameraVideoService.class); try { bindService(intent, p2Connection, Context.BIND_AUTO_CREATE); } catch (Exception e) { Timber.e(e, "bindService P2UsbCameraVideoService failed"); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -129,6 +187,26 @@ if (media == null) { Timber.w("startMedia: media is null"); notifyCallback(1, -1, "MediaArgu is null"); return; } // usbCameraId == 2:走 P2 跨进程服务,支持两路 USB 同时工作 if (media.isUsedOutCamera() && media.getUsbCameraId() == 2) { stopCurrentManager(); ensureP2Bound(); if (p2Service != null) { try { p2Service.startMedia(media); currentManagerType = media.isPush() ? ManagerType.P2_USB_PUSH : ManagerType.P2_USB_RECORD; } catch (RemoteException e) { Timber.e(e, "startMedia forward to P2 failed"); notifyCallback(1, -3, "启动P2服务失败: " + e.getMessage()); } } else { // 等待连接完成后执行 pendingP2StartMedia = media; currentManagerType = media.isPush() ? ManagerType.P2_USB_PUSH : ManagerType.P2_USB_RECORD; } return; } @@ -183,6 +261,18 @@ androidCameraRecordManager.stopRecord(); } break; case P2_USB_PUSH: case P2_USB_RECORD: if (p2Service != null) { try { p2Service.stopMedia(); } catch (RemoteException e) { Timber.e(e, "stopMedia forward to P2 failed"); } } else { pendingP2StartMedia = null; } break; case NONE: break; } @@ -208,6 +298,22 @@ super.onDestroy(); stopMedia(); mCallbacks.kill(); if (p2Bound) { try { if (p2Service != null) { p2Service.unregisterCallback(p2Callback); } } catch (RemoteException e) { Timber.e(e, "Failed to unregister p2Callback"); } try { unbindService(p2Connection); } catch (Exception e) { Timber.w(e, "unbindService P2 failed"); } p2Bound = false; p2Service = null; } Timber.d("FloatingService onDestroy"); } app/src/main/java/com/safeluck/floatwindow/MainActivity.kt
@@ -178,7 +178,7 @@ codeRate = 0 frameRate = 0 m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 url = "rtmp://your-push-url" // TODO: 需要设置实际的推流地址 url = "rtmp://192.168.16.143/live/livestream" // TODO: 需要设置实际的推流地址 userName = "" pwd = "" } app/src/main/java/com/safeluck/floatwindow/MediaArgu.java
@@ -14,8 +14,8 @@ private boolean isPush;//是否推流 true-是 private boolean usedOutCamera;//默认false 使用内置摄像头, true使用外置摄像头 private int usbCameraId;//标记是用1- P1 usb摄像头 2-P2摄像头 private String jsonconfig ; //以后可能会用到,目前用不到 private int codeRate = 0;// 码率 private int frameRate = 0;// 帧率 private ScreenSolution m_screen; @@ -24,8 +24,23 @@ private String pwd;//ftp上传密码 private int recordTime;//分钟 public int getUsbCameraId() { return usbCameraId; } private int tfCardFlag =0; //0-内部flash 1- 外置tfcard public String getJsonconfig() { return jsonconfig; } public void setJsonconfig(String jsonconfig) { this.jsonconfig = jsonconfig; } public void setUsbCameraId(int usbCameraId) { this.usbCameraId = usbCameraId; } private int tfCardFlag =0; //0-内部flash 1- 外置tfcard public int getTfCardFlag() { return tfCardFlag; @@ -181,6 +196,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeByte(this.isPush ? (byte) 1 : (byte) 0); dest.writeByte(this.usedOutCamera ? (byte) 1 : (byte) 0); dest.writeInt(this.usbCameraId); dest.writeInt(this.codeRate); dest.writeInt(this.frameRate); @@ -198,6 +214,7 @@ protected MediaArgu(Parcel in) { this.isPush = in.readByte() != 0; this.usedOutCamera = in.readByte() != 0; this.usbCameraId = in.readInt(); this.codeRate = in.readInt(); this.frameRate = in.readInt(); this.m_screen = in.readParcelable(ScreenSolution.class.getClassLoader()); app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraPushManager.java
@@ -1,6 +1,22 @@ package com.safeluck.floatwindow.manager; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.text.TextUtils; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.WindowManager; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import com.alivc.live.pusher.AlivcAudioAACProfileEnum; import timber.log.Timber; @@ -19,56 +35,67 @@ import com.alivc.live.pusher.AlivcQualityModeEnum; import com.alivc.live.pusher.AlivcResolutionEnum; 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.AudioRecordManager; import com.safeluck.floatwindow.util.GlobalData; /** * USB摄像头推流管理器 */ public class UsbCameraPushManager { private static final String TAG = "UsbCameraPushManager"; private Context context; private MediaArgu mediaArgu; private PushCallback callback; // 阿里推流相关 private AlivcLivePusher alivcPusher; private AlivcLivePushConfig alivcLivePushConfig; private SurfaceView previewSurfaceView; // USB摄像头相关 private UsbCamera usbCamera; private PushThread pushThread; private boolean isRunning = false; private boolean cameraExists = false; private volatile boolean pushStarted = false; // 推流URL private String pushUrl; // 分辨率数组 [width, height] private int[] resolutionArr = new int[]{640, 480}; // 是否开启摄像头加密 private boolean ay_encrypt = false; // 预览 SurfaceView 和隐藏的 Window private WindowManager windowManager; // 音频推流线程池(单线程) private ExecutorService audioPushExecutor; /** * 推流回调接口 */ public interface PushCallback { void onResult(ResponseVO response); } public UsbCameraPushManager(Context context) { this.context = context; } /** * 设置回调 */ public void setCallback(PushCallback callback) { this.callback = callback; } /** * 开始推流 */ @@ -77,111 +104,287 @@ notifyCallback(1, -1, "MediaArgu is null"); return; } this.mediaArgu = media; this.pushUrl = media.getUrl(); if (pushUrl == null || pushUrl.isEmpty()) { notifyCallback(1, -2, "Push URL is empty"); return; } // 设置分辨率 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 { // 初始化推流SDK initAlivcPusher(); setWaterMask(); pushStarted = false; // 检查并打开USB摄像头 if (!openUsbCamera()) { cameraExists = false; notifyCallback(1, -1, "USB摄像头打开失败"); return; } cameraExists = true; Timber.d("USB摄像头打开成功"); // 启动摄像头数据推送线程 startPushThread(); notifyCallback(1, 0, "推流已启动"); notifyCallback(1, 0, "推流线程已启动,等待推流状态就绪"); } catch (Exception e) { Timber.e(e, "Failed to start push"); notifyCallback(1, -3, "启动推流失败: " + e.getMessage()); } } /** * 停止推流 */ public void stopPush() { Timber.d("stopPush called"); stopPushThread(); // stopAudioTransfer(); stopWaterMaskSchedule(); releaseAlivcPusher(); if (usbCamera != null) { usbCamera.stopCamera(); } pushStarted = false; notifyCallback(1, 4, "推流已停止"); } /** * 初始化阿里推流 */ private void initAlivcPusher() { try { alivcLivePushConfig = new AlivcLivePushConfig(); // 根据分辨率设置 setResolutionFromArray(resolutionArr); // 建议用户使用20fps alivcLivePushConfig.setFps(AlivcFpsEnum.FPS_20); // 打开码率自适应 alivcLivePushConfig.setEnableBitrateControl(true); // 设置横屏方向 alivcLivePushConfig.setPreviewOrientation(AlivcPreviewOrientationEnum.ORIENTATION_LANDSCAPE_HOME_LEFT); // 设置音频编码模式 alivcLivePushConfig.setAudioProfile(AlivcAudioAACProfileEnum.AAC_LC); // 设置摄像头类型 alivcLivePushConfig.setCameraType(AlivcLivePushCameraTypeEnum.CAMERA_TYPE_BACK); // 设置视频编码模式为硬编码 alivcLivePushConfig.setVideoEncodeMode(AlivcEncodeModeEnum.Encode_MODE_HARD); // 关闭美颜 alivcLivePushConfig.setBeautyOn(false); // 清晰度优先模式 alivcLivePushConfig.setQualityMode(AlivcQualityModeEnum.QM_RESOLUTION_FIRST); // 设置自定义流模式 alivcLivePushConfig.setExternMainStream(true); alivcLivePushConfig.setAlivcExternMainImageFormat(AlivcImageFormat.IMAGE_FORMAT_YUV420P); // 初始化推流器 alivcPusher = new AlivcLivePusher(); alivcPusher.init(context.getApplicationContext(), alivcLivePushConfig); // 外部自定义流模式下,同样需要先开启预览,让状态从 INIT 进入 PREVIEWED // 创建一个隐藏的 Window 来承载 SurfaceView,确保 Surface 能够被创建 windowManager = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE); previewSurfaceView = new SurfaceView(context.getApplicationContext()); // 在 SurfaceView 的 surfaceCreated 回调中再启动预览,确保 Surface 已经创建 previewSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { try { Timber.d("previewSurfaceView surfaceCreated, startPreviewAysnc"); if (alivcPusher != null) { alivcPusher.startPreviewAysnc(previewSurfaceView); // 启动摄像头数据推送线程 startPushThread(); } } catch (Exception e) { Timber.e(e, "startPreviewAysnc in surfaceCreated failed"); notifyCallback(1, -3, "预览启动失败: " + e.getMessage()); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { Timber.d("previewSurfaceView surfaceChanged: %dx%d", width, height); } @Override public void surfaceDestroyed(SurfaceHolder holder) { Timber.d("previewSurfaceView surfaceDestroyed"); } }); // 将 SurfaceView 添加到隐藏的 Window 中,这样 Surface 才会被创建 WindowManager.LayoutParams params = new WindowManager.LayoutParams( 1, 1, // 1x1 像素,几乎不可见 WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, android.graphics.PixelFormat.TRANSLUCENT ); params.x = -1000; // 移到屏幕外 params.y = -1000; params.alpha = 0.0f; // 完全透明 try { windowManager.addView(previewSurfaceView, params); Timber.d("previewSurfaceView added to window"); } catch (Exception e) { Timber.e(e, "Failed to add previewSurfaceView to window"); // 如果添加失败,尝试使用 TYPE_APPLICATION 类型 params.type = WindowManager.LayoutParams.TYPE_APPLICATION; try { windowManager.addView(previewSurfaceView, params); Timber.d("previewSurfaceView added to window with TYPE_APPLICATION"); } catch (Exception e2) { Timber.e(e2, "Failed to add previewSurfaceView with TYPE_APPLICATION"); } } // 设置监听器 setupListeners(); Timber.d("AlivcPusher initialized successfully"); } catch (Exception e) { Timber.e(e, "Failed to initialize AlivcPusher"); notifyCallback(1, -3, "初始化推流SDK失败: " + e.getMessage()); } } WatermarkParam watermarkParam; ArrayList<WatermarkParam> 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 void setWaterMask() { // 防止重复 schedule(startPush 可能被多次调用) if (watermarkFuture != null && !watermarkFuture.isCancelled()) { return; } if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) { scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); } watermarkFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> { if (pushStarted){ 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 ScheduledFuture<?> watermarkFuture; private void stopWaterMaskSchedule() { 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; } } private Handler mainHandler = new Handler(Looper.getMainLooper()); /** * 设置监听器 */ @@ -191,77 +394,79 @@ @Override public void onPreviewStarted(AlivcLivePusher alivcLivePusher) { Timber.d("onPreviewStarted"); android.os.Handler handler = new android.os.Handler(android.os.Looper.getMainLooper()); handler.postDelayed(new Runnable() { @Override public void run() { if (alivcPusher != null && alivcPusher.getCurrentStatus() != AlivcLivePushStats.PREVIEWED && alivcPusher.getCurrentStatus() != AlivcLivePushStats.PUSHED) { Timber.w("Preview状态异常"); } else { if (cameraExists && pushUrl != null && !pushUrl.isEmpty()) { Timber.d("开始推流: %s", pushUrl); alivcPusher.startPushAysnc(pushUrl); } mainHandler.postDelayed(()->{ // 预览就绪后再启动推流,避免 INIT 状态直接 startPush 报错 if (alivcPusher != null && pushUrl != null && !pushUrl.isEmpty()) { try { AlivcLivePushStats s = alivcPusher.getCurrentStatus(); Timber.i("onPreviewStarted, current status=%s", s != null ? s.name() : "null"); Timber.d("开始推流: %s", pushUrl); alivcPusher.startPushAysnc(pushUrl); } catch (Exception e) { Timber.e(e, "startPushAysnc failed"); notifyCallback(1, -3, "启动推流失败: " + e.getMessage()); } } }, 1000); },1000); } @Override public void onPreviewStoped(AlivcLivePusher alivcLivePusher) { Timber.d("onPreviewStoped"); } @Override public void onPushStarted(AlivcLivePusher alivcLivePusher) { Timber.d("onPushStarted"); pushStarted = true; // startAudioTransfer(); notifyCallback(1, 0, "推流已开始,分辨率: " + resolutionArr[0] + "x" + resolutionArr[1]); } @Override public void onPushPauesed(AlivcLivePusher alivcLivePusher) { Timber.d("onPushPauesed"); } @Override public void onPushResumed(AlivcLivePusher alivcLivePusher) { Timber.d("onPushResumed"); } @Override public void onPushStoped(AlivcLivePusher alivcLivePusher) { Timber.d("onPushStoped"); pushStarted = false; notifyCallback(1, 4, "推流已停止"); } @Override public void onPushRestarted(AlivcLivePusher alivcLivePusher) { Timber.d("onPushRestarted"); } @Override public void onFirstFramePreviewed(AlivcLivePusher alivcLivePusher) { Timber.d("onFirstFramePreviewed"); } @Override public void onDropFrame(AlivcLivePusher alivcLivePusher, int i, int i1) { // 丢帧回调 } @Override public void onAdjustBitRate(AlivcLivePusher alivcLivePusher, int i, int i1) { // 码率调整回调 } @Override public void onAdjustFps(AlivcLivePusher alivcLivePusher, int i, int i1) { // 帧率调整回调 } }); // 错误监听器 alivcPusher.setLivePushErrorListener(new AlivcLivePushErrorListener() { @Override @@ -272,7 +477,7 @@ alivcLivePusher.stopPush(); } } @Override public void onSDKError(AlivcLivePusher alivcLivePusher, AlivcLivePushError alivcLivePushError) { Timber.e("onSDKError: %s", alivcLivePushError.toString()); @@ -282,7 +487,7 @@ } } }); // 网络监听器 alivcPusher.setLivePushNetworkListener(new AlivcLivePushNetworkListener() { @Override @@ -290,54 +495,54 @@ Timber.w("onNetworkPoor"); notifyCallback(1, 3, "网络较差"); } @Override public void onNetworkRecovery(AlivcLivePusher alivcLivePusher) { Timber.d("onNetworkRecovery"); notifyCallback(1, 0, "网络恢复"); } @Override public void onReconnectStart(AlivcLivePusher alivcLivePusher) { Timber.d("onReconnectStart"); } @Override public void onReconnectFail(AlivcLivePusher alivcLivePusher) { Timber.e("onReconnectFail"); notifyCallback(1, 2, "重连失败"); } @Override public void onReconnectSucceed(AlivcLivePusher alivcLivePusher) { Timber.d("onReconnectSucceed"); notifyCallback(1, 0, "重连成功"); } @Override public void onSendDataTimeout(AlivcLivePusher alivcLivePusher) { Timber.w("onSendDataTimeout"); } @Override public void onConnectFail(AlivcLivePusher alivcLivePusher) { Timber.e("onConnectFail"); notifyCallback(1, -2, "连接失败"); } @Override public String onPushURLAuthenticationOverdue(AlivcLivePusher alivcLivePusher) { Timber.w("onPushURLAuthenticationOverdue"); return null; } @Override public void onSendMessage(AlivcLivePusher alivcLivePusher) { // 发送消息回调 } }); } /** * 根据分辨率数组设置分辨率 */ @@ -368,7 +573,7 @@ } } } /** * 打开USB摄像头 */ @@ -381,8 +586,17 @@ // 打开摄像头之前先调用setenv usbCamera.setenv(); // 使用prepareCamera方法,camera_id范围[0,9] int[] cameraIds = {0, 9}; // 使用 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次 @@ -404,7 +618,7 @@ return false; } } /** * 启动推流线程 */ @@ -416,7 +630,7 @@ Timber.d("Push thread started"); } } /** * 停止推流线程 */ @@ -432,17 +646,30 @@ } Timber.d("Push thread stopped"); } /** * 释放阿里推流资源 */ private void releaseAlivcPusher() { // 兜底:防止外部没有走 stopPush stopWaterMaskSchedule(); // 移除隐藏的 SurfaceView if (previewSurfaceView != null && windowManager != null) { try { windowManager.removeView(previewSurfaceView); Timber.d("previewSurfaceView removed from window"); } catch (Exception e) { Timber.e(e, "Error removing previewSurfaceView from window"); } previewSurfaceView = null; } if (alivcPusher != null) { try { AlivcLivePushStats stats = alivcPusher.getCurrentStatus(); Timber.d("当前推流状态: %s", stats != null ? stats.name() : "null"); if (stats != null && (stats == AlivcLivePushStats.PUSHED || if (stats != null && (stats == AlivcLivePushStats.PUSHED || stats == AlivcLivePushStats.PREVIEWED)) { alivcPusher.stopPush(); } @@ -453,8 +680,9 @@ alivcPusher = null; } alivcLivePushConfig = null; pushStarted = false; } /** * 推流线程 */ @@ -463,17 +691,18 @@ public void run() { super.run(); Timber.d("PushThread started"); try { int width = resolutionArr[0]; int height = resolutionArr[1]; final long startTimeNs = System.nanoTime(); // 计算YUV420缓冲区大小 int bufferSize = width * height * 3 / 2; byte[] buffer = new byte[bufferSize]; Timber.d("开始推送视频数据,分辨率: %dx%d", width, height); // 循环处理摄像头数据 while (isRunning && cameraExists) { // 处理摄像头数据 @@ -489,19 +718,23 @@ usbCamera.rgba(1, buffer); // 推流数据到阿里云 if (alivcPusher != null && cameraExists) { if (alivcPusher != null && cameraExists && pushStarted) { try { long ptsUs = (System.nanoTime() - startTimeNs) / 1000; alivcPusher.inputStreamVideoData( buffer, width, height, buffer.length, System.nanoTime() / 1000, // 转换为微秒 ptsUs, // 单调递增的时间戳(微秒) 0 // rotation ); } catch (Exception e) { Timber.e(e, "Error pushing frame"); } } else if (!pushStarted) { // 等待 onPushStarted 后再喂帧,避免 SDK invalid state Thread.sleep(20); } // 控制帧率,约20fps @@ -530,4 +763,52 @@ callback.onResult(response); } } private void startAudioTransfer() { Timber.i("开始通过mic录制声音,上传"); // 创建单线程线程池用于音频推流 if (audioPushExecutor == null || audioPushExecutor.isShutdown()) { audioPushExecutor = Executors.newSingleThreadExecutor(r -> { Thread thread = new Thread(r, "AudioPushThread"); thread.setDaemon(true); return thread; }); } AudioRecordManager.getInstance().startRecording((data, size) -> { if (alivcPusher != null && audioPushExecutor != null && !audioPushExecutor.isShutdown()) { // 在单线程线程池中执行音频推流 audioPushExecutor.execute(() -> { try { alivcPusher.inputStreamAudioData(data, data.length, System.nanoTime() / 1000); } catch (Exception e) { Timber.e(e, "Error pushing audio data"); } }); } }); } private void stopAudioTransfer() { Timber.i("停止通过mic录制声音,上传"); AudioRecordManager.getInstance().stopRecording(); // 停止并关闭音频推流线程池 if (audioPushExecutor != null && !audioPushExecutor.isShutdown()) { audioPushExecutor.shutdown(); try { // 等待最多1秒让任务完成 if (!audioPushExecutor.awaitTermination(1, java.util.concurrent.TimeUnit.SECONDS)) { audioPushExecutor.shutdownNow(); } } catch (InterruptedException e) { audioPushExecutor.shutdownNow(); Thread.currentThread().interrupt(); } audioPushExecutor = null; Timber.d("音频推流线程池已关闭"); } } } app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraRecordManager.java
@@ -8,10 +8,14 @@ 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; @@ -19,6 +23,13 @@ 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摄像头录像管理器 @@ -116,7 +127,7 @@ notifyCallback(0, -1, "USB摄像头打开失败"); return; } setWaterMask(); cameraExists = true; Timber.d("USB摄像头打开成功"); @@ -129,12 +140,122 @@ notifyCallback(0, -3, "启动录像失败: " + e.getMessage()); } } WatermarkParam watermarkParam; ArrayList<WatermarkParam> 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) { @@ -167,8 +288,17 @@ // 打开摄像头之前先调用setenv usbCamera.setenv(); // 使用prepareCamera方法,camera_id范围[0,9] int[] cameraIds = {0, 2}; // 使用 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次 @@ -263,6 +393,7 @@ * 释放资源 */ private void releaseResources() { if (audioRecord != null) { try { if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) { app/src/main/java/com/safeluck/floatwindow/util/AudioRecordManager.java
New file @@ -0,0 +1,123 @@ package com.safeluck.floatwindow.util; import android.media.AudioFormat; import android.media.AudioRecord; import android.media.MediaRecorder; import android.os.Environment; import android.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; /** * aaa * Created by lzw on 2019/8/29. 11:19:57 * 邮箱:632393724@qq.com * All Rights Saved! Chongqing AnYun Tech co. LTD */ public class AudioRecordManager { private static final String TAG = "AudioRecordManager"; private static AudioRecordManager instance; /***标记是否正在录音**/ private boolean isRecording = false; private AudioRecord audioRecord; /***最小缓冲区大小*/ private int bufferSize = 0; /***采样率*/ private int sampleRateInHz = 32000; /***量化位数**/ private int audioFormat = AudioFormat.ENCODING_PCM_16BIT; /***存放音频数据的buffer**/ private byte[] buffer; private int channelConf = AudioFormat.CHANNEL_IN_STEREO; private OnAudioRecordListener onAudioRecordListener; private AudioRecordManager() { //计算最小缓冲区 bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConf, audioFormat); // bufferSize = bufferSize > 320 ? 320 : bufferSize; Log.i(TAG,"mini buffersize="+bufferSize); audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConf, audioFormat, bufferSize); } public static AudioRecordManager getInstance() { if (instance == null) { synchronized (AudioRecordManager.class) { if (instance == null) { instance = new AudioRecordManager(); } } } return instance; } /*** * 开始采集音频 */ public void startRecording(final OnAudioRecordListener onAudioRecordListener) { setAudioRecordListener(onAudioRecordListener); if (audioRecord == null){ audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConf, audioFormat, bufferSize); } buffer = new byte[bufferSize]; isRecording = true; audioRecord.startRecording(); try { while (isRecording){ int readSize = audioRecord.read(buffer,0,bufferSize); Log.i(TAG, "run: buffer length"+buffer.length+" readSize="+readSize); if (onAudioRecordListener != null){ onAudioRecordListener.onVoiceRecord(buffer,bufferSize); } } audioRecord.stop(); destroy(); } catch (Exception e) { e.printStackTrace(); } } /*** * 停止音频采集 */ public void stopRecording(){ Log.i(TAG, "stopRecording"); isRecording = false; setAudioRecordListener(null); } public void destroy(){ if (audioRecord!=null){ audioRecord.release(); audioRecord = null; } } public interface OnAudioRecordListener { /*** * 采集到的音频信息回调到上层 * @param data * @param size */ void onVoiceRecord(byte[] data,int size); } private void setAudioRecordListener(OnAudioRecordListener onAudioRecordListener){ this.onAudioRecordListener = onAudioRecordListener; } } app/src/main/java/com/safeluck/floatwindow/util/GlobalData.java
New file @@ -0,0 +1,128 @@ package com.safeluck.floatwindow.util; import android.text.TextUtils; import android.util.Log; import com.safeluck.floatwindow.MediaArgu; import org.json.JSONException; import org.json.JSONObject; /** * @ProjectName: aaa * @Package: com.safeluck.floatwindow.bean * @ClassName: GlobalData * @Description: 存放全局数据,到处都可以调用; 单例 * @Author: zhanwei.li * @CreateDate: 2021/8/7 10:13 * @UpdateUser: 更新者 * @UpdateDate: 2021/8/7 10:13 * @UpdateRemark: 更新说明 * @Version: 1.0 */ public class GlobalData { private static final String TAG = "GlobalData"; public static final String CPU_MODEL_8953 = "MSM8953"; public String getCameraTag = "_P1"; //水印信息 /* {"teacher": "张三", "student": "李四", "speed": 24.5, "longitude": "26.3231, "latitude": 109.3233, "school": "测试驾校", "car_license": "渝A1234学", "other": "", } */ private String waterMaskInfo; private GlobalData() { } private static GlobalData instance; public static GlobalData getInstance() { if (instance == null) { synchronized (GlobalData.class) { if (instance == null) { instance = new GlobalData(); } } } return instance; } public void setWaterMaskInfo(String info){ Log.i(TAG,"waterMaskInfo="+info); this.waterMaskInfo = info; } public String getWaterMaskInfo(){ if (TextUtils.isEmpty(waterMaskInfo)){ waterMaskInfo = " {\"teacher\": \"\",\n" + " \"student\": "+stuName+",\n" + " \"speed\": 0,\n" + " \"longitude\": \"00.0000,\n" + " \"latitude\": 00.0000,\n" + " \"school\": \"\",\n" + " \"car_license\": \"\",\n" + " \"other\": \"\",\n" + " }"; } return waterMaskInfo; } public<T> T parseWaterMaskInfo(String key,T defaultValue,ShareType type){ Object value = null; Log.i(TAG,"parseWaterMaskInfo="+key); if (!TextUtils.isEmpty(waterMaskInfo)){ try { JSONObject jsonObject = new JSONObject(waterMaskInfo); switch (type){ case STRING: value = jsonObject.getString(key); break; case INTEGER: value = jsonObject.getInt(key); break; case LONG: value = jsonObject.getLong(key); break; case DOUBLE: value = jsonObject.getDouble(key); break; case BOOLEAN: value = jsonObject.getBoolean(key); break; } Log.i(TAG,"key="+key+" value="+value); return (T)value; } catch (JSONException e) { e.printStackTrace(); } } return defaultValue; } private MediaArgu mediaArgu; public MediaArgu getMediaArgu() { return mediaArgu; } public void setMediaArgu(MediaArgu mediaArgu){ this.mediaArgu = mediaArgu; } private String stuName=""; public void setStuName(String userName) { stuName = userName; } public enum ShareType{ STRING,INTEGER,LONG,DOUBLE,BOOLEAN } } readMe.md
New file @@ -0,0 +1,375 @@ * MediaMuxer 启动后不能再添加轨道 * 视频和音频的时间戳需要正确同步 现在代码应该: * 正确等待两个轨道都准备好 * 同步启动 MediaMuxer * 正确写入视频和音频数据 * 避免状态冲突导致的错误 mp4文件明显时间加快了,现实世界1秒钟,可能mp4文件的视频就是半秒,所有录一分钟的视频mp4文件,视频只播到30秒就不再播了 检查并修复时间戳计算问题,可能是帧率或时间戳计算不正确: 修复时间戳计算:改用实际时间而非帧索引,确保时间戳准确: 修复时间戳计算,使用实际时间而非帧索引: UsbCameraRecordManager.java long frameCount = 0; // 记录开始时间(纳秒,用于精确时间戳) long recordingStartTimeNs = System.nanoTime(); long frameCount = 0; UsbCameraRecordManager.java if (videoEncoder != null && mediaMuxer != null) { encodeFrame(buffer, frameCount, width, height); frameCount++; encodeFrame(buffer, frameCount, width, height); if (videoEncoder != null && mediaMuxer != null) { // 计算实际经过的时间(微秒) long elapsedTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000; encodeFrame(buffer, elapsedTimeUs, width, height); frameCount++; UsbCameraRecordManager.java * 编码一帧数据 */ * 编码一帧数据 * @param yuvData YUV数据 * @param presentationTimeUs 时间戳(微秒),基于实际开始时间 * @param width 宽度 * @param height 高度 */ 修复音频时间戳,使其与视频时间戳同步: UsbCameraRecordManager.java +3 /** // 录制开始时间(纳秒),用于时间戳同步 private volatile long recordingStartTimeNs = 0; /** UsbCameraRecordManager.java +1-1 // 记录开始时间(纳秒,用于精确时间戳) long recordingStartTimeNs = System.nanoTime(); long frameCount = 0; long recordingStartTimeNs = System.nanoTime(); // 记录开始时间(纳秒,用于精确时间戳) recordingStartTimeNs = System.nanoTime(); long frameCount = 0; UsbCameraRecordManager.java +3-3 byte[] audioBuffer = new byte[audioBufferSize]; long audioFrameCount = 0; // 编码音频数据 long audioFrameCount = 0; byte[] audioBuffer = new byte[audioBufferSize]; long totalSamplesRead = 0; // 总采样数 // 编码音频数据 UsbCameraRecordManager.java +7-2 * 编码音频数据 */ private void encodeAudio(byte[] audioData, int size, long frameCount) { private void encodeAudio(byte[] audioData, int size, long frameCount) { * 编码音频数据 * @param audioData 音频数据 * @param size 数据大小(字节) * @param totalSamples 总采样数(从开始到现在的累计采样数) */ 在创建新文件时重置开始时间: UsbCameraRecordManager.java +3 // 重新启动音频录制 // 重置开始时间 recordingStartTimeNs = System.nanoTime(); // 重新启动音频录制 已修复时间戳计算问题。主要修改如下:修复内容: * * * 1. 视频时间戳使用实际时间: * 之前:使用 (frameIndex * 1000000) / FRAME_RATE 计算,假设固定帧率 * 现在:使用 (System.nanoTime() - recordingStartTimeNs) / 1000,基于实际经过时间 1. 音频时间戳基于采样数: * 使用 (totalSamples * 1000000) / SAMPLE_RATE 计算 * totalSamples 是累计采样数,确保时间戳连续 1. 添加了录制开始时间记录: * 使用 recordingStartTimeNs 记录开始时间(纳秒) * 在创建新文件时重置,确保每个文件的时间戳从 0 开始 1. 确保时间戳同步: * 视频和音频都从同一个基准时间开始 * 每个新文件的时间戳都从 0 开始 修复原理: * 问题原因:使用帧索引计算时间戳,假设固定帧率。实际采集帧率可能不同,导致时间戳不准确 * 解决方案:使用实际经过的时间计算时间戳,确保时间戳与实际时间一致 现在: * 视频时间戳基于实际经过的时间 * 音频时间戳基于实际读取的采样数 * 两者都从 0 开始,确保同步 #### 使用 private var mediaAidlInterface: IMediaAidlInterface? = null private var serviceConnection: ServiceConnection? = null private var isServiceBound = false private val isServiceBoundState = mutableStateOf(false) serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { mediaAidlInterface = IMediaAidlInterface.Stub.asInterface(service) isServiceBound = true isServiceBoundState.value = true Timber.d("FloatingService connected") } override fun onServiceDisconnected(name: ComponentName?) { mediaAidlInterface = null isServiceBound = false isServiceBoundState.value = false Timber.d("FloatingService disconnected") onBindService = { if (!isServiceBoundState.value && serviceConnection != null) { val intent = Intent(this@MainActivity, FloatingService::class.java) bindService(intent, serviceConnection!!, Context.BIND_AUTO_CREATE) Timber.d("Binding FloatingService") } }, } } private fun startAndroidRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot start Android record") return } try { val mediaArgu = MediaArgu().apply { isPush = false isUsedOutCamera = false // Android 内置摄像头 codeRate = 0 frameRate = 0 m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 recordTime = 0 tfCardFlag = 0 // 内部存储 } mediaAidlInterface?.registerCallback(callback) mediaAidlInterface?.startMedia(mediaArgu) Timber.d("Started Android camera record") } catch (e: RemoteException) { Timber.e(e, "Error starting Android record") } } private fun stopAndroidRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot stop Android record") return } try { mediaAidlInterface?.stopMedia() Timber.d("Stopped Android camera record") } catch (e: RemoteException) { Timber.e(e, "Error stopping Android record") } } private fun startUsbRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot start USB record") return } try { val mediaArgu = MediaArgu().apply { isPush = false isUsedOutCamera = true // USB 摄像头 codeRate = 0 frameRate = 0 m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 recordTime = 0 tfCardFlag = 0 // 内部存储 } mediaAidlInterface?.registerCallback(callback) mediaAidlInterface?.startMedia(mediaArgu) Timber.d("Started USB camera record") } catch (e: RemoteException) { Timber.e(e, "Error starting USB record") } } private fun stopUsbRecord() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot stop USB record") return } try { mediaAidlInterface?.stopMedia() Timber.d("Stopped USB camera record") } catch (e: RemoteException) { Timber.e(e, "Error stopping USB record") } } private fun startUsbPush() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot start USB push") return } try { val mediaArgu = MediaArgu().apply { isPush = true isUsedOutCamera = true // USB 摄像头 codeRate = 0 frameRate = 0 m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 url = "rtmp://192.168.16.143/live/livestream" // TODO: 需要设置实际的推流地址 userName = "" pwd = "" } mediaAidlInterface?.registerCallback(callback) mediaAidlInterface?.startMedia(mediaArgu) Timber.d("Started USB camera push") } catch (e: RemoteException) { Timber.e(e, "Error starting USB push") } } private fun stopUsbPush() { if (mediaAidlInterface == null) { Timber.w("Service not bound, cannot stop USB push") return } try { mediaAidlInterface?.stopMedia() Timber.d("Stopped USB camera push") } catch (e: RemoteException) { Timber.e(e, "Error stopping USB push") } } private fun unbindServiceInternal() { if (isServiceBound && serviceConnection != null) { try { mediaAidlInterface?.unregisterCallback(callback) } catch (e: RemoteException) { Timber.e(e, "Error unregistering callback") } try { unbindService(serviceConnection!!) Timber.d("Unbinding FloatingService") } catch (e: Exception) { Timber.e(e, "Error unbinding service") } isServiceBound = false isServiceBoundState.value = false mediaAidlInterface = null } } usbcameralib/src/main/cpp/watermark.cpp
@@ -66,7 +66,7 @@ pic_width = width; pic_height = height; AppTimer_add(PrintTime, D_SEC(1)); // AppTimer_add(PrintTime, D_SEC(1)); } void UninitWatermark(void)