移动端用户可以通过悬浮的小窗口边观看视频、收听音频,边浏览主屏幕或与其他应用进行交互,实现多前台任务处理。
如果你希望在应用内实现悬浮窗口布局,可以通过 setLocalVideoCanvas 和 setRemoteVideoCanvas 设置画布大小和位置实现,参考 设置视频参数。例如,在 1 v 1 音视频通话中,将远端画面作为背景铺满设备屏幕,同时在屏幕一角展示本端画面。
你已经集成 RTC SDK,实现了基本的音视频通话。
iOS 端已经完成自定义视频渲染器的构建,实现视频画面的自定义渲染。
设备要求:
iOS 16 及以上版本
Android 8.0 及以上版本,API 级别 26
你可以通过构建悬浮窗口在 Android 端实现前台多任务处理。悬浮窗口既可以既用于播放视频,也可以播放纯音频。
你还可以通过 Android 的画中画功能实现多前台任务。
// 悬浮窗需要先请求权限 private void requestFloatingWindowPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Settings.canDrawOverlays(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, REQUEST_CODE_FLOATING_WINDOW); } else { // 已经获得了悬浮窗口权限,可以添加悬浮窗口 addFloatingWindow(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_FLOATING_WINDOW) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Settings.canDrawOverlays(this)) { addFloatingWindow(); } else { ToastUtil.showAlert(this, "未授予悬浮窗口权限,无法添加悬浮窗口"); } } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_FLOATING_WINDOW) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { addFloatingWindow(); } else { ToastUtil.showAlert(this, "未授予悬浮窗口权限,无法添加悬浮窗口"); } } }
private View createFloatWindowView(Context context, @LayoutRes int floatWindowLayoutResId) { return LayoutInflater.from(mContext).inflate(R.layout.layout_float_window, null); }
public FloatWindowManager(Context context, TextureView mainTextureView) { this.context = context; this.mainTextureView = mainTextureView; windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); DisplayMetrics displayMetrics = new DisplayMetrics(); windowManager.getDefaultDisplay().getMetrics(displayMetrics); int windowSizeInPixel = convertDpToPixel(windowSizeInDp, displayMetrics.density); windowParams = new WindowManager.LayoutParams( windowSizeInPixel, windowSizeInPixel, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_PHONE, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT ); windowParams.gravity = Gravity.TOP | Gravity.START; // 视频渲染 View 的容器,将 SDK 进行视频渲染的目标 View 添加到此 ViewGroup 中 floatView = new FrameLayout(context); closeButton = new Button(context); closeButton.setText("Close"); floatView.addView(closeButton, new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL )); floatView.setBackgroundResource(R.color.grey); isWindowOpen = false; // 悬浮窗拖动监听 floatView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: initialTouchX = event.getRawX(); initialTouchY = event.getRawY(); initialWindowX = windowParams.x; initialWindowY = windowParams.y; return true; case MotionEvent.ACTION_MOVE: float dx = event.getRawX() - initialTouchX; float dy = event.getRawY() - initialTouchY; windowParams.x = (int) (initialWindowX + dx); windowParams.y = (int) (initialWindowY + dy); windowManager.updateViewLayout(floatView, windowParams); return true; } return false; } }); }
需要将 RTC SDK 渲染远端视频的 View(TextureView 或 SurfaceView) 添加到 Window 中。
public void openWindow() { if (!isWindowOpen) { floatView.addView(mainTextureView); windowManager.addView(floatView, windowParams); isWindowOpen = true; } }
private void setRemoteRenderView(String uid) { if (textureView.getParent() == null) { remoteViewContainer.removeAllViews(); remoteViewContainer.addView(textureView); } VideoCanvas videoCanvas = new VideoCanvas(); videoCanvas.renderView = textureView; videoCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN; RemoteStreamKey remoteStreamKey = new RemoteStreamKey(roomID, uid, StreamIndex.STREAM_INDEX_MAIN); // 设置远端视频渲染视图 rtcVideo.setRemoteVideoCanvas(remoteStreamKey, videoCanvas); }
// 关闭悬浮窗,并将 SDK 渲染的视频 View 添加到 activity 的页面中 private void closeFloatingWindow() { floatWindowManager.closeWindow(); if (textureView.getParent() != null) { remoteViewContainer.addView(textureView); } } public void closeWindow() { if (isWindowOpen) { floatView.removeView(mainTextureView); windowManager.removeView(floatView); isWindowOpen = false; } }
iOS 端可以通过画中画 (Picture in Picture)功能实现前台多任务处理。
你也可以利用 UIWindow 创建悬浮窗口,实现该功能。
func setupPipController(with sourceView: UIView) { if #available(iOS 16, *) { let callViewController = AVPictureInPictureVideoCallViewController() callViewController.preferredContentSize = CGSize(width: 360, height: 640) callViewController.view.backgroundColor = UIColor.clear let source = AVPictureInPictureController.ContentSource(activeVideoCallSourceView: sourceView, contentViewController: callViewController) let pipVC = AVPictureInPictureController(contentSource: source) pipVC.canStartPictureInPictureAutomaticallyFromInline = true pipVC.delegate = self self.pipVC = pipVC } else { ToastComponents.shared.show(withMessage: "当前系统不支持画中画功能") } }
@objc func startPip() { if self.pipVC!.isPictureInPictureActive { self.pipVC!.stopPictureInPicture() // 关闭画中画 } else { self.pipVC!.startPictureInPicture() } }
// 画中画已经开始 func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) { ToastComponents.shared.show(withMessage: "pictureInPictureControllerDidStart") if #available(iOS 16.0, *) { if let vc = pictureInPictureController.contentSource?.activeVideoCallContentViewController { vc.view.addSubview(self.customRenderView) self.customRenderView.snp.remakeConstraints() { make in make.edges.equalTo(vc.view) } } } }
// 注册外部渲染 func bindRemoteRenderView(view: CustomVideoRenderView, roomId: String, userId: String) { let streamKey = ByteRTCRemoteStreamKey.init() streamKey.userId = userId; streamKey.roomId = roomId; streamKey.streamIndex = .main; // 使用外部渲染 self.rtcVideo?.setRemoteVideoSink(streamKey, withSink: self.customRenderView, withPixelFormat: .original) }
你可以前往示例项目,查看其中的源代码。
平台 | Android | iOS |
---|---|---|
设置本端画面 | setLocalVideoCanvas | setLocalVideoCanvas:withCanvas: |
设置远端画面 | setRemoteVideoCanvas | setRemoteVideoCanvas:withCanvas: |