除提供音视频互动外,你可能还需要将音视频互动录制下来用于内容审核。RTC 提供了云端录制功能。使用此功能,你可以将音视频互动录制下来,并保存到云端。录制过程使用 RTC 提供的云端服务完成,不会占用你的业务服务器算力或客户端设备算力。
在音视频通话场景中,通常需要设置美颜功能。RTC 和 智能美化特效(CV)深度融合,你可以通过调用 RTC SDK 提供的美颜处理接口,快速接入 CV 功能。关于美颜特效,参看美颜特效 CV。
画中画是一种特殊类型的多窗口模式,最常用于视频播放。使用该模式,用户可以通过固定到屏幕一角的小窗口观看视频,同时在应用之间进行导航或浏览主屏幕上的内容。
你可以参考以下示例代码实现画中画模式。
iOS 版本不能低于 15。
保持后台摄像头采集权限
为保持后台摄像头采集,你需为开发者账号添加 entitlement 权限,详情查看multitasking-camera-access
创建画中画控制器
- (void)setupPipControllerWithSourceView:(UIView *)sourceView { if (_pipVC) { [self destroy]; } if (@available(iOS 15, *)) { AVPictureInPictureVideoCallViewController *callViewController = [[AVPictureInPictureVideoCallViewController alloc] init]; callViewController.preferredContentSize = CGSizeMake(720, 1080); callViewController.view.backgroundColor = UIColor.clearColor; AVPictureInPictureControllerContentSource *source = [[AVPictureInPictureControllerContentSource alloc] initWithActiveVideoCallSourceView:sourceView contentViewController:callViewController]; AVPictureInPictureController *pipVC = [[AVPictureInPictureController alloc] initWithContentSource:source]; pipVC.canStartPictureInPictureAutomaticallyFromInline = YES; pipVC.delegate = self; _pipVC = pipVC; } }
- (void)startPIP { if (self.pipViewController.isPictureInPictureActive) { [self.pipViewController stopPictureInPicture]; } else { [self.pipViewController startPictureInPicture]; } }
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController { if (@available(iOS 15.0, *)) { AVPictureInPictureVideoCallViewController *vc = pictureInPictureController.contentSource.activeVideoCallContentViewController; [vc.view addSubview:_renderView]; [_renderView mas_remakeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(vc.view); }]; } }
[self.view.window addSubview:canvas.view]; [canvas.view mas_remakeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view.window); make.size.mas_equalTo(CGSizeMake(100, 200)); }];
Android 版本不能低于 Android 8.0(API 级别 26)
<activity android:name="VideoActivity" android:supportsPictureInPicture="true" android:configChanges= "screenSize|smallestScreenSize|screenLayout|orientation" ...
hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
进行检查以确保可以使用画中画。/**当前系统API是否支持画中画*/ public boolean isSupportPiPMode() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mHost.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); } /***检查画中画权限*/ @RequiresApi(api = Build.VERSION_CODES.O) public boolean hasPiPPermission() { AppOpsManager appOpsManager = (AppOpsManager) mHost.getSystemService(Context.APP_OPS_SERVICE); if (appOpsManager == null) return false; int pid = android.os.Process.myUid(); String packageName = mHost.getPackageName(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { return appOpsManager.unsafeCheckOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, pid, packageName) == AppOpsManager.MODE_ALLOWED; } else { return appOpsManager.checkOpNoThrow(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, pid, packageName) == AppOpsManager.MODE_ALLOWED; } } /***没有画中画权限,使用此方法跳转画中画权限设置页,引导用户开启此权限*/ public void startPiPPermissionSetting(Context context) { try { context.startActivity(new Intent("android.settings.PICTURE_IN_PICTURE_SETTINGS")); } catch (Exception exception) { Log.d(TAG, "start pip permission failed:" + exception); } }
/** * 进入画中画 * @param activity 进入画中画的目标Activity * @param aspectRatio 画中画小窗宽高比 */ public void enterPiPMode(Rational aspectRatio,Activity activity) { PictureInPictureParams mPiPParams = new PictureInPictureParams.Builder() .setAspectRatio(aspectRatio) ...... .build(); activity.enterPictureInPictureMode(mPiPParams); }
Activity.onPictureInPictureModeChanged()
或 Fragment.onPictureInPictureModeChanged()
处理相关UI的展示隐藏。@Override public void onPictureInPictureModeChanged (boolean isInPictureInPictureMode, Configuration newConfig) { if (isInPictureInPictureMode) { // Hide the full-screen UI (controls, etc.) while in picture-in-picture mode. } else { // Restore the full-screen UI. ... } }
/** * 检查是否有悬浮窗权限 */ public static boolean hasPermission() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { return true; } return Settings.canDrawOverlays(AppUtil.getApplicationContext()); } public static final int REQUEST_CODE_OVERLAY = 3001; /** * 如果没有悬浮窗权限,跳转设置中心,引导用户开启 */ public static void startOverlaySetting(Activity activity) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { String packageName = activity.getPackageName(); Uri uri = Uri.parse("package:" + packageName); Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, uri); activity.startActivityForResult(intent, REQUEST_CODE_OVERLAY); } }
提供给 RTC SDK 进行视频渲染的目标 View(TextureView或SurfaceView) 需要布局在 layout_float_window 中。
private View createFloatWindowView(Context context, @LayoutRes int floatWindowLayoutResId) { return LayoutInflater.from(mContext).inflate(R.layout.layout_float_window, null); }
private WindowManager.LayoutParams initFloatWindowLayoutParams() { WindowManager.LayoutParams windowParams = new WindowManager.LayoutParams(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //8.0新特性 windowParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { windowParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; } windowParams.format = PixelFormat.RGBA_8888; windowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; windowParams.gravity = Gravity.START | Gravity.TOP; windowParams.width = WindowManager.LayoutParams.WRAP_CONTENT; windowParams.height = WindowManager.LayoutParams.WRAP_CONTENT; return windowParams; }
public void showFloatWindow(Context context, View floatWindowView, WindowManager.LayoutParams layoutParams) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); if (floatWindowView.getParent() == null) { DisplayMetrics metrics = new DisplayMetrics(); // 默认固定位置,靠屏幕右边缘的中间, 可自己指定 windowManager.getDefaultDisplay().getMetrics(metrics); layoutParams.x = metrics.widthPixels; layoutParams.y = metrics.heightPixels / 2 - getSysBarHeight(); windowManager.addView(floatWindowView, layoutParams); } } private int getSysBarHeight() { int result = 0; Resources resources = AppUtil.getApplicationContext().getResources(); int resourceId = resources.getIdentifier("status_bar_height", "dimen", "android"); if (resourceId > 0) { result = resources.getDimensionPixelSize(resourceId); } return result; }
/** * 渲染视频 * @param rtcVideo RTC引擎实例 * @param renderView 视频渲染目标View,应该属于第2步构建悬浮窗对应UI中的一部分 * @param targetUid 视频渲染目标用户Id * @param roomId RTC房间Id * @param isLocalUser 渲染目标用户是否为本地用户 */ public void renderVideoView(RTCVideo rtcVideo, TextureView renderView, String targetUid, String roomId, boolean isLocalUser) { VideoCanvas videoCanvas = new VideoCanvas(); videoCanvas.renderView = renderView; videoCanvas.roomId = roomId; videoCanvas.uid = targetUid; videoCanvas.isScreen = false; videoCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN; if (isLocalUser) { rtcVideo.setLocalVideoCanvas(StreamIndex.STREAM_INDEX_MAIN, videoCanvas); return; } // 设置远端用户视频渲染视图 rtcVideo.setRemoteVideoCanvas(targetUid, StreamIndex.STREAM_INDEX_MAIN, videoCanvas); }
private void closeFloatWindow(View floatWindowView) { WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); if (floatWindowView.getParent() != null) { windowManager.removeView(floatWindowView); } }