| | |
| | | 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.TimeUnit; |
| | | |
| | | import com.alivc.live.pusher.AlivcAudioAACProfileEnum; |
| | | import timber.log.Timber; |
| | |
| | | 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; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 开始推流 |
| | | */ |
| | |
| | | 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摄像头打开成功"); |
| | | |
| | | |
| | | |
| | | notifyCallback(1, 0, "推流线程已启动,等待推流状态就绪"); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Failed to start push"); |
| | | notifyCallback(1, -3, "启动推流失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 停止推流 |
| | | */ |
| | |
| | | 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 |
| | |
| | | Timber.d("previewSurfaceView surfaceDestroyed"); |
| | | } |
| | | }); |
| | | |
| | | |
| | | // 将 SurfaceView 添加到隐藏的 Window 中,这样 Surface 才会被创建 |
| | | WindowManager.LayoutParams params = new WindowManager.LayoutParams( |
| | | 1, 1, // 1x1 像素,几乎不可见 |
| | |
| | | params.x = -1000; // 移到屏幕外 |
| | | params.y = -1000; |
| | | params.alpha = 0.0f; // 完全透明 |
| | | |
| | | |
| | | try { |
| | | windowManager.addView(previewSurfaceView, params); |
| | | Timber.d("previewSurfaceView added to window"); |
| | |
| | | |
| | | // 设置监听器 |
| | | 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() { |
| | | |
| | | 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 Handler mainHandler = new Handler(Looper.getMainLooper()); |
| | |
| | | },1000); |
| | | |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void onPreviewStoped(AlivcLivePusher alivcLivePusher) { |
| | | Timber.d("onPreviewStoped"); |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void onPushStarted(AlivcLivePusher alivcLivePusher) { |
| | | Timber.d("onPushStarted"); |
| | |
| | | // 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 |
| | |
| | | alivcLivePusher.stopPush(); |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void onSDKError(AlivcLivePusher alivcLivePusher, AlivcLivePushError alivcLivePushError) { |
| | | Timber.e("onSDKError: %s", alivcLivePushError.toString()); |
| | |
| | | } |
| | | } |
| | | }); |
| | | |
| | | |
| | | // 网络监听器 |
| | | alivcPusher.setLivePushNetworkListener(new AlivcLivePushNetworkListener() { |
| | | @Override |
| | |
| | | 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) { |
| | | // 发送消息回调 |
| | | } |
| | | }); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 根据分辨率数组设置分辨率 |
| | | */ |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 打开USB摄像头 |
| | | */ |
| | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 启动推流线程 |
| | | */ |
| | |
| | | Timber.d("Push thread started"); |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 停止推流线程 |
| | | */ |
| | |
| | | } |
| | | Timber.d("Push thread stopped"); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 释放阿里推流资源 |
| | | */ |
| | |
| | | } |
| | | 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(); |
| | | } |
| | |
| | | alivcLivePushConfig = null; |
| | | pushStarted = false; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 推流线程 |
| | | */ |
| | |
| | | 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) { |
| | | // 处理摄像头数据 |