| | |
| | | // void transferInfo(String info); |
| | | void startMedia(in MediaArgu media); |
| | | |
| | | void stopMedia(); |
| | | void stopMedia(in MediaArgu media); |
| | | void sendInfo(String s,int processId);//水印信息 json,processid=1 为发给FloatingService的的信息;procesid=2为发给P2UsbCameraVideoService |
| | | void registerCallback(in IMyCallback cb); |
| | | void unregisterCallback(in IMyCallback cb); |
| | | } |
| | |
| | | import com.safeluck.floatwindow.manager.AndroidCameraRecordManager; |
| | | import com.safeluck.floatwindow.manager.UsbCameraPushManager; |
| | | import com.safeluck.floatwindow.manager.UsbCameraRecordManager; |
| | | import com.safeluck.floatwindow.util.GlobalData; |
| | | |
| | | import timber.log.Timber; |
| | | |
| | |
| | | NONE, |
| | | USB_PUSH, |
| | | USB_RECORD, |
| | | ANDROID_RECORD, |
| | | P2_USB_PUSH, |
| | | P2_USB_RECORD |
| | | ANDROID_RECORD |
| | | } |
| | | |
| | | // AIDL Binder |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void stopMedia() throws RemoteException { |
| | | public void stopMedia(MediaArgu media) throws RemoteException { |
| | | Timber.d("stopMedia called via AIDL"); |
| | | FloatingService.this.stopMedia(); |
| | | FloatingService.this.stopMedia(media); |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void sendInfo(String s, int processId) throws RemoteException { |
| | | if (processId == 2) { |
| | | ensureP2Bound(); |
| | | if (p2Service != null) { |
| | | p2Service.sendInfo(s,processId); |
| | | } |
| | | }else{ |
| | | GlobalData.getInstance().setWaterMaskInfo(s); |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void registerCallback(IMyCallback cb) throws RemoteException { |
| | | if (cb != null) { |
| | |
| | | return; |
| | | } |
| | | |
| | | // usbCameraId == 2:走 P2 跨进程服务,支持两路 USB 同时工作 |
| | | // usbCameraId == 2:走 P2 跨进程服务,支持两路 USB 同时工作(本 Service 不再跟踪其状态) |
| | | 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; |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 停止当前管理器 |
| | | * 停止当前本地管理器(仅管理本进程中的 USB(P1)/Android 录像或推流) |
| | | */ |
| | | private void stopCurrentManager() { |
| | | switch (currentManagerType) { |
| | |
| | | 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; |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * 停止媒体 |
| | | * 停止媒体(通过 AIDL 调用,带 MediaArgu,用于区分 P1/P2) |
| | | */ |
| | | private void stopMedia() { |
| | | Timber.d("stopMedia called"); |
| | | stopCurrentManager(); |
| | | private void stopMedia(MediaArgu media) { |
| | | Timber.d("stopMedia called, media=%s", media); |
| | | if (media != null && media.isUsedOutCamera() && media.getUsbCameraId() == 2) { |
| | | // P2 USB 摄像头由 P2UsbCameraVideoService 管理 |
| | | ensureP2Bound(); |
| | | if (p2Service != null) { |
| | | try { |
| | | p2Service.stopMedia(media); |
| | | } catch (RemoteException e) { |
| | | Timber.e(e, "stopMedia forward to P2 failed"); |
| | | } |
| | | } else { |
| | | pendingP2StartMedia = null; |
| | | } |
| | | } else { |
| | | // 其他情况(包括 P1 USB 与 Android 相机)由本 Service 自己管理 |
| | | stopCurrentManager(); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | |
| | | @Override |
| | | public void onDestroy() { |
| | | super.onDestroy(); |
| | | stopMedia(); |
| | | stopCurrentManager(); |
| | | mCallbacks.kill(); |
| | | if (p2Bound) { |
| | | try { |
| | |
| | | response.setMessage(message); |
| | | notifyCallback(response); |
| | | } |
| | | /* private BroadcastReceiver mCloseBroadCastReceiver = new BroadcastReceiver() { |
| | | @Override |
| | | public void onReceive(Context context, Intent intent) { |
| | | String action = intent.getAction(); |
| | | if (action.equals("com.safeluck.floatwindow_video2.studyinfo")) { |
| | | String str= intent.getStringExtra("info"); |
| | | Log.i(TAG, "广播"+str); |
| | | GlobalData.getInstance().setWaterMaskInfo(str); |
| | | } |
| | | } |
| | | }; |
| | | private void registBroadCastReceiver() { |
| | | IntentFilter filter = new IntentFilter(); |
| | | filter.addAction("com.safeluck.floatwindow_video2.studyinfo"); |
| | | registerReceiver(mCloseBroadCastReceiver,filter); |
| | | |
| | | } |
| | | private void unRegisterTestBroadReceiver() { |
| | | if (mCloseBroadCastReceiver != null) { |
| | | |
| | | //查询到相应的BroadcastReceiver |
| | | unregisterReceiver(mCloseBroadCastReceiver); |
| | | |
| | | } |
| | | }*/ |
| | | } |
| | |
| | | onStartUsbRecord = { startUsbRecord() }, |
| | | onStopUsbRecord = { stopUsbRecord() }, |
| | | onStartUsbPush = { startUsbPush() }, |
| | | onStopUsbPush = { stopUsbPush() } |
| | | onStopUsbPush = { stopUsbPush() }, |
| | | onStartP2Push = { startP2Push() }, |
| | | onStopP2Push = { stopP2Push() }, |
| | | onStartP2Record = { startP2Record() }, |
| | | onStopP2Record = { stopP2Record() } |
| | | ) |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | try { |
| | | mediaAidlInterface?.stopMedia() |
| | | val mediaArgu = MediaArgu().apply { |
| | | isPush = false |
| | | isUsedOutCamera = false // Android 内置摄像头 |
| | | usbCameraId = 0 // Android 相机,usbCameraId 为 0 |
| | | } |
| | | mediaAidlInterface?.stopMedia(mediaArgu) |
| | | Timber.d("Stopped Android camera record") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error stopping Android record") |
| | |
| | | val mediaArgu = MediaArgu().apply { |
| | | isPush = false |
| | | isUsedOutCamera = true // USB 摄像头 |
| | | usbCameraId = 1 // P1 USB 摄像头 |
| | | codeRate = 0 |
| | | frameRate = 0 |
| | | m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 |
| | |
| | | |
| | | mediaAidlInterface?.registerCallback(callback) |
| | | mediaAidlInterface?.startMedia(mediaArgu) |
| | | Timber.d("Started USB camera record") |
| | | Timber.d("Started USB camera record (P1)") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error starting USB record") |
| | | } |
| | |
| | | } |
| | | |
| | | try { |
| | | mediaAidlInterface?.stopMedia() |
| | | Timber.d("Stopped USB camera record") |
| | | val mediaArgu = MediaArgu().apply { |
| | | isPush = false |
| | | isUsedOutCamera = true // USB 摄像头 |
| | | usbCameraId = 1 // P1 USB 摄像头 |
| | | } |
| | | mediaAidlInterface?.stopMedia(mediaArgu) |
| | | Timber.d("Stopped USB camera record (P1)") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error stopping USB record") |
| | | } |
| | |
| | | |
| | | try { |
| | | val mediaArgu = MediaArgu().apply { |
| | | usbCameraId = 1 // P1 USB 摄像头 |
| | | isPush = true |
| | | isUsedOutCamera = true // USB 摄像头 |
| | | codeRate = 0 |
| | |
| | | |
| | | mediaAidlInterface?.registerCallback(callback) |
| | | mediaAidlInterface?.startMedia(mediaArgu) |
| | | Timber.d("Started USB camera push") |
| | | Timber.d("Started USB camera push (P1)") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error starting USB push") |
| | | } |
| | |
| | | } |
| | | |
| | | try { |
| | | mediaAidlInterface?.stopMedia() |
| | | Timber.d("Stopped USB camera push") |
| | | val mediaArgu = MediaArgu().apply { |
| | | usbCameraId = 1 // P1 USB 摄像头 |
| | | isPush = true |
| | | isUsedOutCamera = true // USB 摄像头 |
| | | } |
| | | mediaAidlInterface?.stopMedia(mediaArgu) |
| | | Timber.d("Stopped USB camera push (P1)") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error stopping USB push") |
| | | } |
| | | } |
| | | |
| | | private fun startP2Push() { |
| | | if (mediaAidlInterface == null) { |
| | | Timber.w("Service not bound, cannot start P2 push") |
| | | return |
| | | } |
| | | |
| | | try { |
| | | val mediaArgu = MediaArgu().apply { |
| | | usbCameraId = 2 // P2 USB 摄像头 |
| | | 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 P2 USB camera push") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error starting P2 push") |
| | | } |
| | | } |
| | | |
| | | private fun stopP2Push() { |
| | | if (mediaAidlInterface == null) { |
| | | Timber.w("Service not bound, cannot stop P2 push") |
| | | return |
| | | } |
| | | |
| | | try { |
| | | val mediaArgu = MediaArgu().apply { |
| | | usbCameraId = 2 // P2 USB 摄像头 |
| | | isPush = true |
| | | isUsedOutCamera = true // USB 摄像头 |
| | | } |
| | | mediaAidlInterface?.stopMedia(mediaArgu) |
| | | Timber.d("Stopped P2 USB camera push") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error stopping P2 push") |
| | | } |
| | | } |
| | | |
| | | private fun startP2Record() { |
| | | if (mediaAidlInterface == null) { |
| | | Timber.w("Service not bound, cannot start P2 record") |
| | | return |
| | | } |
| | | |
| | | try { |
| | | val mediaArgu = MediaArgu().apply { |
| | | isPush = false |
| | | isUsedOutCamera = true // USB 摄像头 |
| | | usbCameraId = 2 // P2 USB 摄像头 |
| | | codeRate = 0 |
| | | frameRate = 0 |
| | | m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率 |
| | | recordTime = 0 |
| | | tfCardFlag = 0 // 内部存储 |
| | | } |
| | | |
| | | mediaAidlInterface?.registerCallback(callback) |
| | | mediaAidlInterface?.startMedia(mediaArgu) |
| | | Timber.d("Started P2 USB camera record") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error starting P2 record") |
| | | } |
| | | } |
| | | |
| | | private fun stopP2Record() { |
| | | if (mediaAidlInterface == null) { |
| | | Timber.w("Service not bound, cannot stop P2 record") |
| | | return |
| | | } |
| | | |
| | | try { |
| | | val mediaArgu = MediaArgu().apply { |
| | | isPush = false |
| | | isUsedOutCamera = true // USB 摄像头 |
| | | usbCameraId = 2 // P2 USB 摄像头 |
| | | } |
| | | mediaAidlInterface?.stopMedia(mediaArgu) |
| | | Timber.d("Stopped P2 USB camera record") |
| | | } catch (e: RemoteException) { |
| | | Timber.e(e, "Error stopping P2 record") |
| | | } |
| | | } |
| | | |
| | |
| | | onStartUsbRecord: () -> Unit, |
| | | onStopUsbRecord: () -> Unit, |
| | | onStartUsbPush: () -> Unit, |
| | | onStopUsbPush: () -> Unit |
| | | onStopUsbPush: () -> Unit, |
| | | onStartP2Push: () -> Unit, |
| | | onStopP2Push: () -> Unit, |
| | | onStartP2Record: () -> Unit, |
| | | onStopP2Record: () -> Unit |
| | | ) { |
| | | Column( |
| | | modifier = Modifier.fillMaxSize() |
| | |
| | | |
| | | Divider(modifier = Modifier.padding(vertical = 8.dp)) |
| | | |
| | | // 按钮 7: 开始 USB 推流 |
| | | // 按钮 7: 开始 USB 推流 (P1) |
| | | Button( |
| | | onClick = onStartUsbPush, |
| | | modifier = Modifier.fillMaxWidth(), |
| | | enabled = isServiceBound |
| | | ) { |
| | | Text("7. 开始 USB 推流") |
| | | Text("7. 开始 USB 推流 (P1)") |
| | | } |
| | | |
| | | // 按钮 8: 结束 USB 推流 |
| | | // 按钮 8: 结束 USB 推流 (P1) |
| | | Button( |
| | | onClick = onStopUsbPush, |
| | | modifier = Modifier.fillMaxWidth(), |
| | | enabled = isServiceBound |
| | | ) { |
| | | Text("8. 结束 USB 推流") |
| | | Text("8. 结束 USB 推流 (P1)") |
| | | } |
| | | |
| | | Divider(modifier = Modifier.padding(vertical = 8.dp)) |
| | | |
| | | // 按钮 9: 开始 P2 USB 推流 |
| | | Button( |
| | | onClick = onStartP2Push, |
| | | modifier = Modifier.fillMaxWidth(), |
| | | enabled = isServiceBound |
| | | ) { |
| | | Text("9. 开始 P2 USB 推流") |
| | | } |
| | | |
| | | // 按钮 10: 结束 P2 USB 推流 |
| | | Button( |
| | | onClick = onStopP2Push, |
| | | modifier = Modifier.fillMaxWidth(), |
| | | enabled = isServiceBound |
| | | ) { |
| | | Text("10. 结束 P2 USB 推流") |
| | | } |
| | | |
| | | Divider(modifier = Modifier.padding(vertical = 8.dp)) |
| | | |
| | | // 按钮 11: 开始 P2 USB 录像 |
| | | Button( |
| | | onClick = onStartP2Record, |
| | | modifier = Modifier.fillMaxWidth(), |
| | | enabled = isServiceBound |
| | | ) { |
| | | Text("11. 开始 P2 USB 录像") |
| | | } |
| | | |
| | | // 按钮 12: 结束 P2 USB 录像 |
| | | Button( |
| | | onClick = onStopP2Record, |
| | | modifier = Modifier.fillMaxWidth(), |
| | | enabled = isServiceBound |
| | | ) { |
| | | Text("12. 结束 P2 USB 录像") |
| | | } |
| | | |
| | | Spacer(modifier = Modifier.height(12.dp)) |
| New file |
| | |
| | | package com.safeluck.floatwindow; |
| | | |
| | | import android.app.Service; |
| | | import android.content.Context; |
| | | import android.content.Intent; |
| | | import android.os.Handler; |
| | | import android.os.IBinder; |
| | | import android.os.Looper; |
| | | import android.os.RemoteCallbackList; |
| | | import android.os.RemoteException; |
| | | |
| | | import com.safeluck.floatwindow.manager.UsbCameraPushManager; |
| | | import com.safeluck.floatwindow.manager.UsbCameraRecordManager; |
| | | import com.safeluck.floatwindow.util.GlobalData; |
| | | |
| | | import timber.log.Timber; |
| | | |
| | | /** |
| | | * 运行在独立进程(:p2)的 USB 摄像头视频服务,用于同时支持两路 USB 摄像头工作。 |
| | | * 约定:当 MediaArgu.usbCameraId == 2 时,调用方应使用该服务进行推流/录像。 |
| | | */ |
| | | public class P2UsbCameraVideoService extends Service { |
| | | private static final String TAG = "P2UsbCameraVideoService"; |
| | | |
| | | private Context context; |
| | | |
| | | private UsbCameraPushManager usbCameraPushManager; |
| | | private UsbCameraRecordManager usbCameraRecordManager; |
| | | |
| | | private ManagerType currentManagerType = ManagerType.NONE; |
| | | |
| | | // 主线程 Handler,用于确保 AlivcLivePusher 初始化在主线程执行 |
| | | private final Handler mainHandler = new Handler(Looper.getMainLooper()); |
| | | |
| | | private enum ManagerType { |
| | | NONE, |
| | | USB_PUSH, |
| | | USB_RECORD |
| | | } |
| | | |
| | | private final RemoteCallbackList<IMyCallback> mCallbacks = new RemoteCallbackList<>(); |
| | | |
| | | private final IMediaAidlInterface.Stub mBinder = new IMediaAidlInterface.Stub() { |
| | | @Override |
| | | public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) { |
| | | Timber.d("[%s] basicTypes called", TAG); |
| | | } |
| | | |
| | | @Override |
| | | public void startMedia(MediaArgu media) throws RemoteException { |
| | | Timber.d("[%s] startMedia called via AIDL", TAG); |
| | | P2UsbCameraVideoService.this.startMedia(media); |
| | | } |
| | | |
| | | @Override |
| | | public void stopMedia(MediaArgu media) throws RemoteException { |
| | | Timber.d("[%s] stopMedia called via AIDL", TAG); |
| | | P2UsbCameraVideoService.this.stopMedia(media); |
| | | } |
| | | |
| | | @Override |
| | | public void sendInfo(String s, int processId) throws RemoteException { |
| | | GlobalData.getInstance().setWaterMaskInfo(s); |
| | | } |
| | | |
| | | @Override |
| | | public void registerCallback(IMyCallback cb) throws RemoteException { |
| | | if (cb != null) { |
| | | mCallbacks.register(cb); |
| | | Timber.d("[%s] Callback registered", TAG); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void unregisterCallback(IMyCallback cb) throws RemoteException { |
| | | if (cb != null) { |
| | | mCallbacks.unregister(cb); |
| | | Timber.d("[%s] Callback unregistered", TAG); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | @Override |
| | | public void onCreate() { |
| | | super.onCreate(); |
| | | context = this; |
| | | |
| | | usbCameraPushManager = new UsbCameraPushManager(context); |
| | | usbCameraPushManager.setCallback(this::notifyCallback); |
| | | |
| | | usbCameraRecordManager = new UsbCameraRecordManager(context); |
| | | usbCameraRecordManager.setCallback(this::notifyCallback); |
| | | |
| | | Timber.d("[%s] onCreate", TAG); |
| | | } |
| | | |
| | | @Override |
| | | public int onStartCommand(Intent intent, int flags, int startId) { |
| | | return START_STICKY; |
| | | } |
| | | |
| | | @Override |
| | | public IBinder onBind(Intent intent) { |
| | | Timber.d("[%s] onBind", TAG); |
| | | return mBinder; |
| | | } |
| | | |
| | | @Override |
| | | public void onDestroy() { |
| | | super.onDestroy(); |
| | | stopCurrentManager(); |
| | | mCallbacks.kill(); |
| | | Timber.d("[%s] onDestroy", TAG); |
| | | } |
| | | |
| | | private void startMedia(MediaArgu media) { |
| | | if (media == null) { |
| | | notifyCallback(1, -1, "MediaArgu is null"); |
| | | return; |
| | | } |
| | | |
| | | // 该服务只处理 USB 摄像头 |
| | | if (!media.isUsedOutCamera()) { |
| | | notifyCallback(1, -4, "P2 service only supports USB camera"); |
| | | return; |
| | | } |
| | | |
| | | // 确保在主线程执行,因为 AlivcLivePusher 初始化需要 Handler(必须在有 Looper 的线程) |
| | | if (Looper.myLooper() == Looper.getMainLooper()) { |
| | | startMediaInternal(media); |
| | | } else { |
| | | mainHandler.post(() -> startMediaInternal(media)); |
| | | } |
| | | } |
| | | |
| | | private void startMediaInternal(MediaArgu media) { |
| | | // 先停止当前 |
| | | stopCurrentManager(); |
| | | |
| | | if (media.isPush()) { |
| | | currentManagerType = ManagerType.USB_PUSH; |
| | | usbCameraPushManager.startPush(media); |
| | | } else { |
| | | currentManagerType = ManagerType.USB_RECORD; |
| | | usbCameraRecordManager.startRecord(media); |
| | | } |
| | | } |
| | | |
| | | private void stopMedia(MediaArgu media) { |
| | | // 确保在主线程执行 |
| | | if (Looper.myLooper() == Looper.getMainLooper()) { |
| | | stopCurrentManager(); |
| | | } else { |
| | | mainHandler.post(this::stopCurrentManager); |
| | | } |
| | | } |
| | | |
| | | private void stopCurrentManager() { |
| | | switch (currentManagerType) { |
| | | case USB_PUSH: |
| | | if (usbCameraPushManager != null) { |
| | | usbCameraPushManager.stopPush(); |
| | | } |
| | | break; |
| | | case USB_RECORD: |
| | | if (usbCameraRecordManager != null) { |
| | | usbCameraRecordManager.stopRecord(); |
| | | } |
| | | break; |
| | | case NONE: |
| | | break; |
| | | } |
| | | currentManagerType = ManagerType.NONE; |
| | | } |
| | | |
| | | private void notifyCallback(ResponseVO response) { |
| | | if (response == null) return; |
| | | int count = mCallbacks.beginBroadcast(); |
| | | for (int i = 0; i < count; i++) { |
| | | try { |
| | | mCallbacks.getBroadcastItem(i).onResult(response); |
| | | } catch (RemoteException e) { |
| | | Timber.e(e, "[%s] Error notifying callback", TAG); |
| | | } |
| | | } |
| | | mCallbacks.finishBroadcast(); |
| | | } |
| | | |
| | | private void notifyCallback(int type, int errCode, String message) { |
| | | ResponseVO response = new ResponseVO(); |
| | | response.setType(type); |
| | | response.setErrCode(errCode); |
| | | response.setMessage(message); |
| | | notifyCallback(response); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | try { |
| | | // 创建新的视频文件 |
| | | currentVideoFile = VideoFileUtils.getVideoFile(context, mediaArgu.getTfCardFlag()); |
| | | currentVideoFile = VideoFileUtils.getVideoFile(context, mediaArgu.getTfCardFlag(),0); |
| | | if (currentVideoFile == null) { |
| | | Timber.e("Failed to create video file"); |
| | | return; |
| | |
| | | int fontSize= 24; |
| | | private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); |
| | | private void setWaterMask() { |
| | | |
| | | if (mediaArgu.getUsbCameraId()==2){ |
| | | GlobalData.getInstance().getCameraTag="_P2"; |
| | | }else{ |
| | | GlobalData.getInstance().getCameraTag="_P1"; |
| | | } |
| | | // 防止重复 schedule(startPush 可能被多次调用) |
| | | if (watermarkFuture != null && !watermarkFuture.isCancelled()) { |
| | | return; |
| | |
| | | double speed = GlobalData.getInstance().parseWaterMaskInfo("speed", 0.0, GlobalData.ShareType.DOUBLE); |
| | | |
| | | |
| | | String czh = GlobalData.getInstance().parseWaterMaskInfo("car_license", "无", GlobalData.ShareType.STRING) + GlobalData.getInstance().getCameraTag; |
| | | 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); |
| | |
| | | int usbId = (mediaArgu != null) ? mediaArgu.getUsbCameraId() : 0; |
| | | int[] cameraIds; |
| | | if (usbId == 2) { |
| | | cameraIds = new int[]{2}; |
| | | cameraIds = new int[]{2,3}; |
| | | } else if (usbId == 1) { |
| | | cameraIds = new int[]{0}; |
| | | cameraIds = new int[]{1,2}; |
| | | } else { |
| | | cameraIds = new int[]{0, 2}; |
| | | } |
| | |
| | | private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); |
| | | private ScheduledFuture<?> watermarkFuture; |
| | | private void setWaterMask() { |
| | | |
| | | if (mediaArgu.getUsbCameraId()==2){ |
| | | GlobalData.getInstance().getCameraTag="_P2"; |
| | | }else{ |
| | | GlobalData.getInstance().getCameraTag="_P1"; |
| | | } |
| | | // 防止重复 schedule(startRecord 可能被多次调用) |
| | | if (watermarkFuture != null && !watermarkFuture.isCancelled()) { |
| | | return; |
| | |
| | | int usbId = (mediaArgu != null) ? mediaArgu.getUsbCameraId() : 0; |
| | | int[] cameraIds; |
| | | if (usbId == 2) { |
| | | cameraIds = new int[]{2}; |
| | | cameraIds = new int[]{2,3}; |
| | | } else if (usbId == 1) { |
| | | cameraIds = new int[]{0}; |
| | | cameraIds = new int[]{1,2}; |
| | | } else { |
| | | cameraIds = new int[]{0, 2}; |
| | | cameraIds = new int[]{0, 1}; |
| | | } |
| | | String cameraName = null; // 不指定特定名称 |
| | | |
| | |
| | | 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); |
| | | // P2 摄像头(usbCameraId == 2)不录制音频,因为麦克风只有一个,只在 P1 时使用 |
| | | boolean enableAudio = (mediaArgu == null || mediaArgu.getUsbCameraId() != 2); |
| | | |
| | | 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; |
| | | if (enableAudio) { |
| | | // 创建音频编码器 |
| | | 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; |
| | | } |
| | | Timber.d("音频编码器和AudioRecord初始化成功(P1模式)"); |
| | | } else { |
| | | // P2 模式:不初始化音频相关资源 |
| | | audioEncoder = null; |
| | | audioRecord = null; |
| | | audioTrackIndex = -1; |
| | | Timber.d("P2模式:跳过音频初始化,仅录制视频"); |
| | | } |
| | | |
| | | // 创建新的视频文件 |
| | | currentVideoFile = VideoFileUtils.getVideoFile(context, mediaArgu.getTfCardFlag()); |
| | | currentVideoFile = VideoFileUtils.getVideoFile(context, mediaArgu.getTfCardFlag(),mediaArgu.getUsbCameraId()); |
| | | 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; |
| | | if (enableAudio) { |
| | | audioTrackIndex = -1; // 只有在启用音频时才初始化为-1,否则保持-1(表示无音频轨道) |
| | | } |
| | | muxerStarted = false; |
| | | currentFileStartTime = System.currentTimeMillis(); |
| | | |
| | | Timber.d("编码器和Muxer初始化成功,文件: %s", currentVideoFile.getAbsolutePath()); |
| | | Timber.d("编码器和Muxer初始化成功,文件: %s, 音频: %s", |
| | | currentVideoFile.getAbsolutePath(), enableAudio ? "启用" : "禁用"); |
| | | return true; |
| | | } catch (Exception e) { |
| | | Timber.e(e, "初始化编码器和Muxer失败"); |
| | |
| | | return; |
| | | } |
| | | |
| | | // 启动音频录制 |
| | | if (audioRecord != null) { |
| | | // P2模式(usbCameraId == 2)不启动音频录制 |
| | | boolean isP2Mode = (mediaArgu != null && mediaArgu.getUsbCameraId() == 2); |
| | | if (!isP2Mode && audioRecord != null) { |
| | | // 启动音频录制 |
| | | audioRecord.startRecording(); |
| | | Timber.d("音频录制已启动"); |
| | | Timber.d("音频录制已启动(P1模式)"); |
| | | |
| | | // 注意:不要在这里主动检查音频编码器输出格式 |
| | | // 因为 MediaCodec 的 getOutputFormat() 在编码器启动后可能返回 null |
| | | // 应该等待 INFO_OUTPUT_FORMAT_CHANGED 事件 |
| | | |
| | | // 启动音频编码线程 |
| | | audioThread = new AudioThread(); |
| | | audioThread.start(); |
| | | } else { |
| | | Timber.d("P2模式:跳过音频录制和编码线程"); |
| | | } |
| | | |
| | | // 注意:不要在这里主动检查音频编码器输出格式 |
| | | // 因为 MediaCodec 的 getOutputFormat() 在编码器启动后可能返回 null |
| | | // 应该等待 INFO_OUTPUT_FORMAT_CHANGED 事件 |
| | | |
| | | // 启动音频编码线程 |
| | | audioThread = new AudioThread(); |
| | | audioThread.start(); |
| | | |
| | | Timber.d("开始录像,分辨率: %dx%d", width, height); |
| | | |
| | |
| | | // 重置开始时间 |
| | | recordingStartTimeNs = System.nanoTime(); |
| | | |
| | | // 重新启动音频录制 |
| | | if (audioRecord != null) { |
| | | // P2模式不启动音频录制 |
| | | isP2Mode = (mediaArgu != null && mediaArgu.getUsbCameraId() == 2); |
| | | if (!isP2Mode && audioRecord != null) { |
| | | // 重新启动音频录制 |
| | | audioRecord.startRecording(); |
| | | audioThread = new AudioThread(); |
| | | audioThread.start(); |
| | | } |
| | | audioThread = new AudioThread(); |
| | | audioThread.start(); |
| | | |
| | | lastFileChangeTime = currentTime; |
| | | frameCount = 0; |
| | |
| | | } |
| | | |
| | | /** |
| | | * 检查并启动Muxer(当视频和音频轨道都准备好时) |
| | | * 检查并启动Muxer |
| | | * P1模式:当视频和音频轨道都准备好时启动 |
| | | * P2模式:当视频轨道准备好时即可启动(无音频轨道) |
| | | */ |
| | | 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"); |
| | | boolean isP2Mode = (mediaArgu != null && mediaArgu.getUsbCameraId() == 2); |
| | | |
| | | if (isP2Mode) { |
| | | // P2模式:只要有视频轨道就启动 |
| | | if (!muxerStarted && videoTrackIndex >= 0) { |
| | | try { |
| | | mediaMuxer.start(); |
| | | muxerStarted = true; |
| | | Timber.d("Muxer started (P2模式,仅视频), video track: %d", videoTrackIndex); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Failed to start muxer"); |
| | | } |
| | | } else { |
| | | Timber.d("Muxer not started yet (P2模式), video track: %d", videoTrackIndex); |
| | | } |
| | | } else { |
| | | Timber.d("Muxer not started yet, video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex); |
| | | // P1模式:需要视频和音频轨道都准备好 |
| | | if (!muxerStarted && videoTrackIndex >= 0 && audioTrackIndex >= 0) { |
| | | try { |
| | | mediaMuxer.start(); |
| | | muxerStarted = true; |
| | | Timber.d("Muxer started (P1模式,视频+音频), 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 (P1模式), video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | super.run(); |
| | | Timber.d("AudioThread started"); |
| | | |
| | | // 如果音频资源未初始化(P2模式),直接退出 |
| | | if (audioRecord == null || audioEncoder == null) { |
| | | Timber.d("AudioThread: 音频资源未初始化,退出(可能是P2模式)"); |
| | | return; |
| | | } |
| | | |
| | | try { |
| | | byte[] audioBuffer = new byte[audioBufferSize]; |
| | | long totalSamplesRead = 0; // 总采样数 |
| | |
| | | * 生成视频文件名(时分秒.mp4) |
| | | * @return 文件名,例如:143025.mp4 |
| | | */ |
| | | public static String generateVideoFileName() { |
| | | public static String generateVideoFileName(int usbCamId) { |
| | | SimpleDateFormat timeFormat = new SimpleDateFormat("HHmmss", Locale.getDefault()); |
| | | return timeFormat.format(new Date()) + ".mp4"; |
| | | if (usbCamId==2){ |
| | | return timeFormat.format(new Date()) + "_P2.mp4"; |
| | | }else if (usbCamId==1){ |
| | | return timeFormat.format(new Date()) + "_P1.mp4"; |
| | | }else{ |
| | | return timeFormat.format(new Date()) + ".mp4"; |
| | | } |
| | | |
| | | } |
| | | |
| | | /** |
| | |
| | | * @param tfCardFlag 0-内部存储,1-外部存储 |
| | | * @return 完整的文件路径 |
| | | */ |
| | | public static File getVideoFile(Context context, int tfCardFlag) { |
| | | public static File getVideoFile(Context context, int tfCardFlag,int usbCameraId) { |
| | | File dateDir = getVideoDirectory(context, tfCardFlag); |
| | | if (dateDir == null) { |
| | | return null; |
| | | } |
| | | |
| | | String fileName = generateVideoFileName(); |
| | | String fileName = generateVideoFileName(usbCameraId); |
| | | return new File(dateDir, fileName); |
| | | } |
| | | |
| | | /** |
| | | * 获取视频文件路径字符串 |
| | | * 获取视频文件路径字符串 只能获取到内部相机文件的路径 |
| | | * @param context 上下文 |
| | | * @param tfCardFlag 0-内部存储,1-外部存储 |
| | | * @param cameraid 0-android系统相机 1-P1 2-P2 |
| | | * @return 文件路径字符串 |
| | | */ |
| | | public static String getVideoFilePath(Context context, int tfCardFlag) { |
| | | File file = getVideoFile(context, tfCardFlag); |
| | | public static String getVideoFilePath(Context context, int tfCardFlag,int cameraid) { |
| | | File file = getVideoFile(context, tfCardFlag,cameraid); |
| | | return file != null ? file.getAbsolutePath() : null; |
| | | } |
| | | } |