移动端用户可以通过悬浮的小窗口边观看视频、收听音频,边浏览主屏幕或与其他应用进行交互,实现多前台任务处理。
如果你希望在应用内实现悬浮窗口布局,可以通过 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: |