除提供音视频互动外,你可能还需要将音视频互动录制下来用于内容审核。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;
}
}
objectivec- (void)startPIP {
if (self.pipViewController.isPictureInPictureActive) {
[self.pipViewController stopPictureInPicture];
} else {
[self.pipViewController startPictureInPicture];
}
}
objectivec- (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);
}];
}
}
objectivec[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));
}];
objectivecAndroid 版本不能低于 Android 8.0(API 级别 26)
<activity android:name="VideoActivity"
android:supportsPictureInPicture="true"
android:configChanges=
"screenSize|smallestScreenSize|screenLayout|orientation"
...
JavahasSystemFeature(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);
}
}
Java/**
* 进入画中画
* @param activity 进入画中画的目标Activity
* @param aspectRatio 画中画小窗宽高比
*/
public void enterPiPMode(Rational aspectRatio,Activity activity) {
PictureInPictureParams mPiPParams = new PictureInPictureParams.Builder()
.setAspectRatio(aspectRatio)
......
.build();
activity.enterPictureInPictureMode(mPiPParams);
}
JavaActivity.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.
...
}
}
Java /**
* 检查是否有悬浮窗权限
*/
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);
}
}
Java提供给 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);
}
Javaprivate 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;
}
Javapublic 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;
}
Java/**
* 渲染视频
* @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);
}
javaprivate void closeFloatWindow(View floatWindowView) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
if (floatWindowView.getParent() != null) {
windowManager.removeView(floatWindowView);
}
}