You need to enable JavaScript to run this app.
导航

体验进阶

最近更新时间2023.10.08 17:46:42

首次发布时间2023.07.19 10:32:43

进阶功能

云端录制

除提供音视频互动外,你可能还需要将音视频互动录制下来用于内容审核。RTC 提供了云端录制功能。使用此功能,你可以将音视频互动录制下来,并保存到云端。录制过程使用 RTC 提供的云端服务完成,不会占用你的业务服务器算力或客户端设备算力。

智能美化特效

在音视频通话场景中,通常需要设置美颜功能。RTC 和 智能美化特效(CV)深度融合,你可以通过调用 RTC SDK 提供的美颜处理接口,快速接入 CV 功能。关于美颜特效,参看美颜特效 CV

最佳实践

画中画

画中画是一种特殊类型的多窗口模式,最常用于视频播放。使用该模式,用户可以通过固定到屏幕一角的小窗口观看视频,同时在应用之间进行导航或浏览主屏幕上的内容。
你可以参考以下示例代码实现画中画模式。

iOS

iOS 版本不能低于 15。

应用外小窗(系统 PIP)

  1. 保持后台摄像头采集权限
    为保持后台摄像头采集,你需为开发者账号添加 entitlement 权限,详情查看multitasking-camera-access

  2. 创建画中画控制器

- (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;
    }
}
  1. 使用系统 API 开启画中画
- (void)startPIP {
    if (self.pipViewController.isPictureInPictureActive) {
        [self.pipViewController stopPictureInPicture];
    } else {
        [self.pipViewController startPictureInPicture];
    }
}
  1. 开启画中画,将Canvas.view加载到画中画上
- (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);
        }];
    }
}

应用内小窗(自定义 PIP)

[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 版本不能低于 Android 8.0(API 级别 26)

  1. 配置相关属性:
    画中画 API 的维度为 Activity,因此需要在 AndroidManifest.xml 中为对应 Activity 添加如下属性。
<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges=
        "screenSize|smallestScreenSize|screenLayout|orientation"
    ...
  1. 功能支持检测及权限校验:
    1. 低 RAM 设备可能无法使用画中画模式,在应用使用画中画之前,请务必通过调用 hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) 进行检查以确保可以使用画中画。
    2. 是否有画中画权限,如果没有可以引导用户开启:
/**当前系统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);
    }
}
  1. 进入画中画模式
    通过画中画相关参数PictureInPictureParams设置画中画宽高比等参数,调用系统API进入画中画模式。
/**
 * 进入画中画
 * @param activity 进入画中画的目标Activity
 * @param aspectRatio 画中画小窗宽高比
 */
public void enterPiPMode(Rational aspectRatio,Activity activity) {
    PictureInPictureParams mPiPParams = new PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
             ......
            .build();
    activity.enterPictureInPictureMode(mPiPParams);
}
  1. 处理进出画中画模式相关UI
    监听系统回调 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.
        ...
    }
}

音频流(悬浮窗 PIP)

  1. 检查是否有悬浮窗展示权限
/**
 * 检查是否有悬浮窗权限
 */

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);
    }
}
  1. 构建悬浮窗对应UI

提供给 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);
}
  1. 构建悬浮窗布局参数
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;
}
  1. 通过 WindowManager 展示悬浮窗
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;
}
  1. 渲染视频
/**
 * 渲染视频
 * @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);
}
  1. 隐藏悬浮窗
private void closeFloatWindow(View floatWindowView) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    if (floatWindowView.getParent() != null) {
        windowManager.removeView(floatWindowView);
    }
}