在实时通信中,如果你希望用户可以分享本端设备的屏幕和设备播放的音频,可以使用 RTC 内建的屏幕采集功能,也可以自行实现屏幕采集逻辑(自定义采集),并通过屏幕共享功能,与远端用户共享。
在使用屏幕共享功能时,仅可见的用户可以发布屏幕流。
你可以在多种行业、多种场景下使用屏幕共享功能:
行业 | 场景 |
---|---|
在线教育 | 老师共享屏幕给学生上课;美术老师共享屏幕给学生教画画。 |
游戏直播 | 主播共享屏幕给观众,展现自己的游戏画面。 |
互动直播 | 主播共享自己的屏幕和观众互动。 |
视频会议 | 会议成员共享屏幕观看 PPT 或者文档。 |
Android 端屏幕共享基于 Android 5 (API 级别 21) 中引入的媒体投影 API 和 RTC 提供的 API 共同实现。
你已经集成 Android SDK,实现了基本的音视频通话。
Android 5.0 (API 级别 21) 及以上版本。推荐使用 Android 10 (API 级别 29) 及以上版本
说明:Android 5.0 ~ 10 版本仅支持屏幕视频采集,不支持屏幕音频采集。
Android 10 (API 级别 29) 及以上进行屏幕采集需要用到前台服务,在应用的 Android 清单文件中添加如下前台服务声明。
录音权限声明已包含在 RTC SDK 中,App 清单文件无需添加。
<application> ... <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> ... <service android:name="com.ss.bytertc.base.media.screen.RXScreenCaptureService" android:enabled="true" android:exported="false" android:foregroundServiceType="mediaProjection" /> </application>
Android 14(API 级别 34)及以上还需声明如下权限,以免出现应用闪退等异常:
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
向系统请求屏幕共享的权限。
public static final int REQUEST_CODE_OF_SCREEN_SHARING = 101; // 向系统发起屏幕共享的权限请求 public void requestForScreenSharing() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { Log.e("ShareScreen","当前系统版本过低,无法支持屏幕共享"); return; } MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); if (projectionManager != null) { startActivityForResult(projectionManager.createScreenCaptureIntent(), REQUEST_CODE_OF_SCREEN_SHARING); } else { Log.e("ShareScreen","当前系统版本过低,无法支持屏幕共享"); } }
在权限申请的响应中,开启屏幕共享。
public static final int REQUEST_CODE_OF_SCREEN_SHARING = 101; @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == REQUEST_CODE_OF_SCREEN_SHARING && resultCode == Activity.RESULT_OK) { startScreenShare(data); } else { super.onActivityResult(requestCode, resultCode, data); } }
private void startScreenShare(Intent data) { startRXScreenCaptureService(data); //编码参数 ScreenVideoEncoderConfig config = new ScreenVideoEncoderConfig(); config.width = 720; config.height = 1280; config.frameRate = 15; config.maxBitrate = 1600; mRTCVideo.setScreenVideoEncoderConfig(config); // 开启屏幕视频数据采集 mRTCVideo.startScreenCapture(ScreenMediaType.SCREEN_MEDIA_TYPE_VIDEO_AND_AUDIO, data); } private void startRXScreenCaptureService(@NonNull Intent data) { Context context = Utilities.getApplicationContext(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { Intent intent = new Intent(); intent.putExtra(RXScreenCaptureService.KEY_LARGE_ICON, R.drawable.launcher_quick_start); intent.putExtra(RXScreenCaptureService.KEY_SMALL_ICON, R.drawable.launcher_quick_start); intent.putExtra(RXScreenCaptureService.KEY_LAUNCH_ACTIVITY, mHostActivity.getClass().getCanonicalName()); intent.putExtra(RXScreenCaptureService.KEY_CONTENT_TEXT, "正在录制/投射您的屏幕"); intent.putExtra(RXScreenCaptureService.KEY_RESULT_DATA, data); context.startForegroundService(RXScreenCaptureService.getServiceIntent(context, RXScreenCaptureService.COMMAND_LAUNCH, intent)); } }
onVideoDeviceStateChanged
中调用 publishScreen
。@Override public void onVideoDeviceStateChanged(String deviceId, VideoDeviceType deviceType, int deviceState, int deviceError) { if (deviceType == VideoDeviceType.VIDEO_DEVICE_TYPE_SCREEN_CAPTURE_DEVICE) { if (deviceState == MediaDeviceState.MEDIA_DEVICE_STATE_STARTED) { mRTCRoom.publishScreen(MediaStreamType.RTC_MEDIA_STREAM_TYPE_BOTH); mRTCVideo.setVideoSourceType(StreamIndex.STREAM_INDEX_SCREEN, VideoSourceType.VIDEO_SOURCE_TYPE_INTERNAL); } else if (deviceState == MediaDeviceState.MEDIA_DEVICE_STATE_STOPPED || deviceState == MediaDeviceState.MEDIA_DEVICE_STATE_RUNTIMEERROR) { mRTCRoom.unpublishScreen(MediaStreamType.RTC_MEDIA_STREAM_TYPE_BOTH); } } }
//SDK收到第一帧远端视频解码数据后,用户收到此回调。 @Override public void onFirstRemoteVideoFrameDecoded(RemoteStreamKey remoteStreamKey, VideoFrameInfo frameInfo) { super.onFirstRemoteVideoFrameDecoded(remoteStreamKey, frameInfo); runOnUiThread(() -> setRemoteView(remoteStreamKey)); }
private void setRemoteRenderView(RemoteStreamKey remoteStreamKey, FrameLayout container) { TextureView renderView = new TextureView(this); FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); container.removeAllViews(); container.addView(renderView, params); VideoCanvas videoCanvas = new VideoCanvas(); videoCanvas.renderView = renderView; videoCanvas.roomId = remoteStreamKey.getRoomId(); videoCanvas.uid = remoteStreamKey.getUserId(); videoCanvas.renderMode = VideoCanvas.RENDER_MODE_HIDDEN; // 设置远端用户视频渲染视图 mInstance.setRemoteVideoCanvas(remoteStreamKey.getUserId(), remoteStreamKey.getStreamIndex(), videoCanvas); }
private void stopScreenShare() { // 停止屏幕数据采集 mInstance.stopScreenCapture(); }
// 房间内远端屏幕共享音视频流移除的回调。 @Override public void onUserUnPublishScreen(String uid, MediaStreamType type, StreamRemoveReason reason) { super.onUserUnPublishScreen(uid, type, reason); runOnUiThread(() -> removeRemoteView(getScreenShareUserId(uid))); }
RTC 强烈建议你使用内部采集。如果你仍然希望使用自定义采集,参看以下步骤。
实现屏幕音视频流采集逻辑。
指定为外部输入源。
setScreenAudioSourceType
设置屏幕音频自定义采集。setVideoSourceType
设置屏幕视频自定义采集。调用 pushScreenAudioFrame
和 pushScreenVideoFrame
将采集得到的音视频帧推送到 RTC SDK 用于编码传输。
共享屏幕后,对端只能看到画面,听不到音频。
收到以下错误提示:Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
原因和解决方法如下:
AndroidManifest.xml
中配置 android.permission.FOREGROUND_SERVICE
权限和 org.webrtc.RXScreenCaptureService
声明。RXScreenCaptureService
时,RXScreenCaptureService.KEY_LAUNCH_ACTIVITY
是否传入了全类名。 由于启动流程中开启 notification 时,需要获取全类名,以通过反射获取相关联的 activity,非全类名将导致 notification 创建失败,并收到上述错误提示。没有实现功能,且无其他错误提示。
RXScreenCaptureService
时,RXScreenCaptureService.KEY_RESULT_DATA
是否传入 。使用 Android 5.0 ~ 10 版本 设备进行屏幕共享后自动断开。
上述系统版本的个别 Android 机型可能存在兼容性问题。建议用户升级系统版本。
本文最近更新时的 SDK 版本为 3.50.1。如果你使用的 SDK 为之前版本,请查看以下变动,并进行相应适配。
ScreenVideoEncoderConfig
。RTCEngine
变更为 RTCRoom
。其他方法的类名由 RTCEngine
变更为 RTCVideo
。IRTCEngineEventHandler
变更为 IRTCVideoEventHandler
。setVideoEncoderConfig
变更为 setScreenVideoEncoderConfig
。publishScreen
等 API 的参数有变更。