| | |
| | | // 音频推流线程池(单线程) |
| | | private ExecutorService audioPushExecutor; |
| | | |
| | | // 保护 alivcPusher / previewSurfaceView 等生命周期,避免 start/stop 并发导致 NPE |
| | | private final Object pusherLock = new Object(); |
| | | |
| | | /** |
| | | * 推流回调接口 |
| | | */ |
| | |
| | | } |
| | | |
| | | /** |
| | | * 开始推流 |
| | | * 开始推流(对外接口) |
| | | * 注意:AlivcLivePusher 必须在主线程初始化,否则会抛出 |
| | | * "Can't create handler inside thread that has not called Looper.prepare()" |
| | | */ |
| | | public void startPush(MediaArgu media) { |
| | | // 确保在主线程执行实际的启动逻辑 |
| | | if (Looper.myLooper() == Looper.getMainLooper()) { |
| | | startPushInternal(media); |
| | | } else { |
| | | // 当前是 Binder 线程或其他后台线程,切到主线程 |
| | | mainHandler.post(() -> startPushInternal(media)); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 真正的启动推流逻辑,必须在主线程调用 |
| | | */ |
| | | private void startPushInternal(MediaArgu media) { |
| | | if (media == null) { |
| | | notifyCallback(1, -1, "MediaArgu is null"); |
| | | 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]); |
| | | Timber.d("%s 设置分辨率: %dx%d", getCameraTag(), resolutionArr[0], resolutionArr[1]); |
| | | } |
| | | |
| | | try { |
| | | // 初始化推流SDK |
| | | // 初始化推流SDK(此时已保证在主线程) |
| | | initAlivcPusher(); |
| | | setWaterMask(); |
| | | pushStarted = false; |
| | |
| | | } |
| | | |
| | | cameraExists = true; |
| | | Timber.d("USB摄像头打开成功"); |
| | | |
| | | Timber.d("%s USB摄像头打开成功", getCameraTag()); |
| | | |
| | | notifyCallback(1, 0, "推流线程已启动,等待推流状态就绪"); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Failed to start push"); |
| | | Timber.e(e, "%s Failed to start push", getCameraTag()); |
| | | notifyCallback(1, -3, "启动推流失败: " + e.getMessage()); |
| | | } |
| | | } |
| | |
| | | * 停止推流 |
| | | */ |
| | | public void stopPush() { |
| | | Timber.d("stopPush called"); |
| | | stopPushThread(); |
| | | // stopAudioTransfer(); |
| | | stopWaterMaskSchedule(); |
| | | releaseAlivcPusher(); |
| | | if (usbCamera != null) { |
| | | usbCamera.stopCamera(); |
| | | // stop 同样强制在主线程串行执行,避免与 init/setupListeners 并发 |
| | | if (Looper.myLooper() == Looper.getMainLooper()) { |
| | | stopPushInternal(); |
| | | } else { |
| | | mainHandler.post(this::stopPushInternal); |
| | | } |
| | | pushStarted = false; |
| | | notifyCallback(1, 4, "推流已停止"); |
| | | } |
| | | |
| | | private void stopPushInternal() { |
| | | synchronized (pusherLock) { |
| | | Timber.d("%s stopPush called", getCameraTag()); |
| | | stopPushThread(); |
| | | // stopAudioTransfer(); |
| | | stopWaterMaskSchedule(); |
| | | releaseAlivcPusherLocked(); |
| | | if (usbCamera != null) { |
| | | usbCamera.stopCamera(); |
| | | } |
| | | pushStarted = false; |
| | | notifyCallback(1, 4, "推流已停止"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 初始化阿里推流 |
| | | */ |
| | | private void initAlivcPusher() { |
| | | try { |
| | | alivcLivePushConfig = new AlivcLivePushConfig(); |
| | | synchronized (pusherLock) { |
| | | try { |
| | | alivcLivePushConfig = new AlivcLivePushConfig(); |
| | | |
| | | // 根据分辨率设置 |
| | | setResolutionFromArray(resolutionArr); |
| | | // 根据分辨率设置 |
| | | setResolutionFromArray(resolutionArr); |
| | | |
| | | // 建议用户使用20fps |
| | | alivcLivePushConfig.setFps(AlivcFpsEnum.FPS_20); |
| | | // 建议用户使用20fps |
| | | alivcLivePushConfig.setFps(AlivcFpsEnum.FPS_20); |
| | | |
| | | // 打开码率自适应 |
| | | alivcLivePushConfig.setEnableBitrateControl(true); |
| | | // 打开码率自适应 |
| | | alivcLivePushConfig.setEnableBitrateControl(true); |
| | | |
| | | // 设置横屏方向 |
| | | alivcLivePushConfig.setPreviewOrientation(AlivcPreviewOrientationEnum.ORIENTATION_LANDSCAPE_HOME_LEFT); |
| | | // 设置横屏方向 |
| | | alivcLivePushConfig.setPreviewOrientation(AlivcPreviewOrientationEnum.ORIENTATION_LANDSCAPE_HOME_LEFT); |
| | | |
| | | // 设置音频编码模式 |
| | | alivcLivePushConfig.setAudioProfile(AlivcAudioAACProfileEnum.AAC_LC); |
| | | // 设置音频编码模式 |
| | | alivcLivePushConfig.setAudioProfile(AlivcAudioAACProfileEnum.AAC_LC); |
| | | |
| | | // 设置摄像头类型 |
| | | alivcLivePushConfig.setCameraType(AlivcLivePushCameraTypeEnum.CAMERA_TYPE_BACK); |
| | | // 设置摄像头类型 |
| | | alivcLivePushConfig.setCameraType(AlivcLivePushCameraTypeEnum.CAMERA_TYPE_BACK); |
| | | |
| | | // 设置视频编码模式为硬编码 |
| | | alivcLivePushConfig.setVideoEncodeMode(AlivcEncodeModeEnum.Encode_MODE_HARD); |
| | | // 设置视频编码模式为硬编码 |
| | | alivcLivePushConfig.setVideoEncodeMode(AlivcEncodeModeEnum.Encode_MODE_HARD); |
| | | |
| | | // 关闭美颜 |
| | | alivcLivePushConfig.setBeautyOn(false); |
| | | // 关闭美颜 |
| | | alivcLivePushConfig.setBeautyOn(false); |
| | | |
| | | // 清晰度优先模式 |
| | | alivcLivePushConfig.setQualityMode(AlivcQualityModeEnum.QM_RESOLUTION_FIRST); |
| | | // 清晰度优先模式 |
| | | alivcLivePushConfig.setQualityMode(AlivcQualityModeEnum.QM_RESOLUTION_FIRST); |
| | | |
| | | // 设置自定义流模式 |
| | | alivcLivePushConfig.setExternMainStream(true); |
| | | alivcLivePushConfig.setAlivcExternMainImageFormat(AlivcImageFormat.IMAGE_FORMAT_YUV420P); |
| | | // 设置自定义流模式 |
| | | 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()); |
| | | // 初始化推流器:先用局部变量,避免中途被 stop 置空导致 setupListeners NPE |
| | | AlivcLivePusher localPusher = new AlivcLivePusher(); |
| | | localPusher.init(context.getApplicationContext(), alivcLivePushConfig); |
| | | |
| | | // 在 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); |
| | | // 外部自定义流模式下,同样需要先开启预览,让状态从 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("%s previewSurfaceView surfaceCreated, startPreviewAysnc", getCameraTag()); |
| | | synchronized (pusherLock) { |
| | | if (alivcPusher != null && previewSurfaceView != null) { |
| | | alivcPusher.startPreviewAysnc(previewSurfaceView); |
| | | |
| | | |
| | | // 启动摄像头数据推送线程 |
| | | startPushThread(); |
| | | // 启动摄像头数据推送线程 |
| | | startPushThread(); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | Timber.e(e, "%s startPreviewAysnc in surfaceCreated failed", getCameraTag()); |
| | | notifyCallback(1, -3, "预览启动失败: " + e.getMessage()); |
| | | } |
| | | } 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"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { |
| | | Timber.d("previewSurfaceView surfaceChanged: %dx%d", width, height); |
| | | } |
| | | // 设置监听器(对局部 pusher 先绑定),最后再发布到字段 |
| | | setupListeners(localPusher); |
| | | alivcPusher = localPusher; |
| | | |
| | | @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"); |
| | | Timber.d("%s AlivcPusher initialized successfully", getCameraTag()); |
| | | } 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"); |
| | | } |
| | | Timber.e(e, "%s Failed to initialize AlivcPusher", getCameraTag()); |
| | | notifyCallback(1, -3, "初始化推流SDK失败: " + e.getMessage()); |
| | | } |
| | | |
| | | // 设置监听器 |
| | | setupListeners(); |
| | | |
| | | Timber.d("AlivcPusher initialized successfully"); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Failed to initialize AlivcPusher"); |
| | | notifyCallback(1, -3, "初始化推流SDK失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | |
| | | /** |
| | | * 设置监听器 |
| | | */ |
| | | private void setupListeners() { |
| | | private void setupListeners(AlivcLivePusher pusher) { |
| | | if (pusher == null) { |
| | | Timber.w("%s setupListeners skipped: pusher is null", getCameraTag()); |
| | | return; |
| | | } |
| | | // 推流信息监听器 |
| | | alivcPusher.setLivePushInfoListener(new AlivcLivePushInfoListener() { |
| | | pusher.setLivePushInfoListener(new AlivcLivePushInfoListener() { |
| | | @Override |
| | | public void onPreviewStarted(AlivcLivePusher alivcLivePusher) { |
| | | Timber.d("onPreviewStarted"); |
| | | Timber.d("%s onPreviewStarted", getCameraTag()); |
| | | 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); |
| | | Timber.i("%s onPreviewStarted, current status=%s", getCameraTag(), s != null ? s.name() : "null"); |
| | | Timber.d("%s 开始推流: %s", getCameraTag(), pushUrl); |
| | | alivcPusher.startPushAysnc(pushUrl); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "startPushAysnc failed"); |
| | | Timber.e(e, "%s startPushAysnc failed", getCameraTag()); |
| | | notifyCallback(1, -3, "启动推流失败: " + e.getMessage()); |
| | | } |
| | | } |
| | |
| | | |
| | | @Override |
| | | public void onPushStarted(AlivcLivePusher alivcLivePusher) { |
| | | Timber.d("onPushStarted"); |
| | | Timber.d("%s onPushStarted", getCameraTag()); |
| | | pushStarted = true; |
| | | // startAudioTransfer(); |
| | | notifyCallback(1, 0, "推流已开始,分辨率: " + resolutionArr[0] + "x" + resolutionArr[1]); |
| | |
| | | }); |
| | | |
| | | // 错误监听器 |
| | | alivcPusher.setLivePushErrorListener(new AlivcLivePushErrorListener() { |
| | | pusher.setLivePushErrorListener(new AlivcLivePushErrorListener() { |
| | | @Override |
| | | public void onSystemError(AlivcLivePusher alivcLivePusher, AlivcLivePushError alivcLivePushError) { |
| | | Timber.e("onSystemError: %s", alivcLivePushError.toString()); |
| | |
| | | }); |
| | | |
| | | // 网络监听器 |
| | | alivcPusher.setLivePushNetworkListener(new AlivcLivePushNetworkListener() { |
| | | pusher.setLivePushNetworkListener(new AlivcLivePushNetworkListener() { |
| | | @Override |
| | | public void onNetworkPoor(AlivcLivePusher alivcLivePusher) { |
| | | Timber.w("onNetworkPoor"); |
| | |
| | | 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]); |
| | | Timber.d("%s USB摄像头第%d次打开结果: %d, 分辨率: %dx%d", getCameraTag(), i + 1, ret, resolutionArr[0], resolutionArr[1]); |
| | | if (ret == 0) { |
| | | break; |
| | | } |
| | |
| | | // 成功标准:prepareCamera 返回 0 |
| | | return ret == 0; |
| | | } catch (Exception e) { |
| | | Timber.e(e, "打开USB摄像头异常"); |
| | | Timber.e(e, "%s 打开USB摄像头异常", getCameraTag()); |
| | | return false; |
| | | } |
| | | } |
| | |
| | | isRunning = true; |
| | | pushThread = new PushThread(); |
| | | pushThread.start(); |
| | | Timber.d("Push thread started"); |
| | | Timber.d("%s Push thread started", getCameraTag()); |
| | | } |
| | | } |
| | | |
| | |
| | | * 释放阿里推流资源 |
| | | */ |
| | | private void releaseAlivcPusher() { |
| | | synchronized (pusherLock) { |
| | | releaseAlivcPusherLocked(); |
| | | } |
| | | } |
| | | |
| | | private void releaseAlivcPusherLocked() { |
| | | // 兜底:防止外部没有走 stopPush |
| | | stopWaterMaskSchedule(); |
| | | // 移除隐藏的 SurfaceView |
| | |
| | | Timber.d("当前推流状态: %s", stats != null ? stats.name() : "null"); |
| | | |
| | | if (stats != null && (stats == AlivcLivePushStats.PUSHED || |
| | | stats == AlivcLivePushStats.PREVIEWED)) { |
| | | stats == AlivcLivePushStats.PREVIEWED)) { |
| | | alivcPusher.stopPush(); |
| | | } |
| | | alivcPusher.destroy(); |
| | |
| | | @Override |
| | | public void run() { |
| | | super.run(); |
| | | Timber.d("PushThread started"); |
| | | Timber.d("%s PushThread started", getCameraTag()); |
| | | |
| | | try { |
| | | int width = resolutionArr[0]; |
| | |
| | | int bufferSize = width * height * 3 / 2; |
| | | byte[] buffer = new byte[bufferSize]; |
| | | |
| | | Timber.d("开始推送视频数据,分辨率: %dx%d", width, height); |
| | | Timber.d("%s 开始推送视频数据,分辨率: %dx%d", getCameraTag(), width, height); |
| | | |
| | | // 循环处理摄像头数据 |
| | | while (isRunning && cameraExists) { |
| | | // 处理摄像头数据 |
| | | int processResult = usbCamera.processCamera(); |
| | | if (processResult == -1) { |
| | | Timber.w("processCamera返回-1,摄像头可能断开"); |
| | | Timber.w("%s processCamera返回-1,摄像头可能断开", getCameraTag()); |
| | | cameraExists = false; |
| | | notifyCallback(1, -1, "USB摄像头断开"); |
| | | break; |
| | |
| | | 0 // rotation |
| | | ); |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Error pushing frame"); |
| | | Timber.e(e, "%s Error pushing frame", getCameraTag()); |
| | | } |
| | | } else if (!pushStarted) { |
| | | // 等待 onPushStarted 后再喂帧,避免 SDK invalid state |
| | |
| | | } |
| | | |
| | | } catch (Exception e) { |
| | | Timber.e(e, "Error in push thread"); |
| | | Timber.e(e, "%s Error in push thread", getCameraTag()); |
| | | cameraExists = false; |
| | | notifyCallback(1, -1, "推流线程异常: " + e.getMessage()); |
| | | } finally { |
| | | Timber.d("PushThread ended"); |
| | | Timber.d("%s PushThread ended", getCameraTag()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取摄像头ID标签,用于日志 |
| | | * @return "[P1]" 或 "[P2]" |
| | | */ |
| | | private String getCameraTag() { |
| | | if (mediaArgu != null) { |
| | | int usbCameraId = mediaArgu.getUsbCameraId(); |
| | | return usbCameraId == 2 ? "[P2]" : "[P1]"; |
| | | } |
| | | return "[P1]"; // 默认 P1 |
| | | } |
| | | |
| | | /** |
| | |
| | | response.setType(type); |
| | | response.setErrCode(errCode); |
| | | response.setMessage(message); |
| | | // 设置 cameraId:根据 usbCameraId 区分 P1(1) 和 P2(2) |
| | | if (mediaArgu != null) { |
| | | int usbCameraId = mediaArgu.getUsbCameraId(); |
| | | response.setCameraId(usbCameraId == 2 ? 2 : 1); // 2 -> P2, 其他 -> P1 |
| | | } else { |
| | | response.setCameraId(1); // 默认 P1 |
| | | } |
| | | callback.onResult(response); |
| | | } |
| | | } |