Android端实现屏幕共享

本文将为您介绍Android端如何实现屏幕共享。

功能介绍

屏幕共享功能允许用户在视频通话、直播过程中将自己的屏幕内容实时分享给频道内的其他用户,实现信息的即时共享与可视化交流。

示例项目

ARTC 提供了开源示例项目供您参考,您可以前往下载或查看其中的代码示例代码:Android实现屏幕共享

前提条件

在实现屏幕共享前,请确保达成以下条件:

  • 一个有效的阿里云账号并创建实时音视频应用,请参考创建应用。在应用管理控制台获取App IDApp Key。

  • 已经在项目中集成了 ARTC SDK,并实现了基础的实时音视频功能。SDK 集成请参考SDK下载/集成,实现音视频请参考:实现音视频通话

  • 受系统限制,采集屏幕仅适用于 Android API 级别为 21 及以上,即 Android 5 及以上,否则在调用 startScreenShare时,SDK 会报告开启失败。

  • 受系统限制,采集系统音频仅适用于 Android API 级别为 29 及以上,即 Android 10 及以上,否则会采集音频失败。

注意事项

  • 某些品牌的部分型号手机不支持共享系统音频或共享出来的音频质量很差。比如小米,且某些能正常录音的小米设备在使用特定APP(QQ Music)录制系统音频会有杂音。

  • 某些品牌的部分设备在屏幕共享中不支持切换分辨率,否则会崩溃,比如华为。

建议在多款设备上充分测试,确保屏幕共享功能稳定。

功能实现

实现屏幕共享的调用时序如下:

image

1.设置权限

为了在 Android 平台上实现稳定、兼容的屏幕共享功能,应用需在 AndroidManifest.xml 中添加以下权限声明与 Service 注册信息。

  • FOREGROUND_SERVICE:用于启动前台服务,保证在后台状态下屏幕共享服务不被系统杀死。

  • FOREGROUND_SERVICE_MEDIA_PROJECTION:Android 10(API 29)及以上版本引入的精细化权限,用于标识前台服务类型为屏幕采集(MediaProjection)。Android 14(API 级别 34)及以上必须声明该权限。

  • POST_NOTIFICATIONS:从 Android 13(API 33)开始,应用如果需要发送通知(例如前台服务通知),必须声明此权限,未授权将无法启动前台服务通知。

  • Service 声明:要实现前台服务,需要在AndroidManifest.xml 中声明对应的 Service 组件,否则,系统无法识别该服务,启动时会报错或者失败。

说明

屏幕共享权限是由系统通过一个标准的授权界面(系统弹窗)向用户申请,必须用户主动同意才能使用。且该授权是一次性的,每次启动屏幕共享时应用都需要触发授权流程。

<manifest ...>
    <!-- 声明前台服务权限 -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
    <!-- Android 13 (API级别33)以上 -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> 
    
    <application ...>
          <!-- 声明用于屏幕共享的前台Service -->
          <service android:name="com.alivc.rtc.share.ForegroundService"
            android:enabled="true" />
    </application>
</manifest>

2.相机流与屏幕共享流配置

ARTC 支持推送相机流和屏幕共享流,请根据业务场景灵活配置。

2.1.仅需推送屏幕共享流

如果您的场景中如果仅需要推送屏幕流,由于 SDK 默认会默认推送相机流,因此需要您主动调用publishLocalVideoStream关闭相机流推送,流程如下:

  • 加入频道前调用publishLocalVideoStream(false)来关闭相机流推送。

  • 加入频道后调用startScreenShare开启屏幕录制并推送屏幕共享流。

mAliRtcEngine.publishLocalVideoStream(false);

2.2.需要同时推送屏幕共享流和相机流

如果您的场景中需要同时推送相机流和屏幕共享流,需要:

调用publishLocalVideoStream(true)开启相机流推送。(默认行为,可省略)。加入频道后调用startScreenShare开启屏幕录制并推送屏幕共享流。

mAliRtcEngine.publishLocalVideoStream(true);

3.设置屏幕流编码配置

如果需要自定义屏幕共享视频流的编码属性,可以调用setScreenShareEncoderConfiguration接口进行配置,包含分辨率、帧率、码率、GOP、视频旋转方向。

说明
  • 该接口在加入频道前后均可配置,如果每次入会只需要设置一次屏幕流视频编码属性,建议在入会前调用。

  • 如果需要更新配置,可以多次调用该接口。

相关配置如下:

参数名

参数说明

默认值

dimensions

视频分辨率。

0x0,表示推流分辨率跟随屏幕采集的分辨率。最大取值为3840x2160。

frameRate

视频帧率。

默认帧率为 5,最大取值为 30。

bitrate

视频编码码率(Kbps)。

说明

码率设置根据分辨率和帧率有对应的合理范围,该值设置在合理范围内有效,否则SDK会自动调节码率到有效值

512

keyFrameInterval

关键帧间隔,GOP。单位为毫秒(ms)。

默认值0,表示SDK内部控制关键帧间隔。

forceStrictKeyFrameInterval

是否强制编码器严格按照设置的关键帧间隔产生关键帧。

默认值为 false。

  • false表示编码器会响应他人入会等关键帧请求,关键帧间隔和设置的值不严格匹配。

  • true表示编码器不响应其他关键帧请求,严格按照设置的值产生关键帧。可能会造成订阅者首帧变慢。

rotationMode

推流旋转

默认值为AliRtcRotationMode_0。可选择 0、90、180、270 四个角度。

示例代码如下:

AliRtcEngine.AliRtcScreenShareEncoderConfiguration screenShareEncoderConfiguration = new AliRtcEngine.AliRtcScreenShareEncoderConfiguration();
screenShareEncoderConfiguration.bitrate = bitrate;
screenShareEncoderConfiguration.dimensions = new AliRtcEngine.AliRtcVideoDimensions(width, height);
screenShareEncoderConfiguration.frameRate = fps;
screenShareEncoderConfiguration.keyFrameInterval = gop;
screenShareEncoderConfiguration.forceStrictKeyFrameInterval= mForceKeyFrameSwitch.isChecked();
mAliRtcEngine.setScreenShareEncoderConfiguration(screenShareEncoderConfiguration);

4.开始屏幕采集

调用 startScreenShare 接口启动屏幕采集流程,开始采集设备屏幕内容并推送至通话频道。根据你的应用场景进行参数设置:

  1. intent:外部创建启动屏幕分享的Activity,外部未创建则传null,建议传null。

  2. screenShareMode:屏幕共享模式,只共享音频/视频/音视频。

说明

应用调用此接口启动屏幕采集时,系统会弹出一个系统级别的授权弹窗,提示用户是否允许当前应用捕获屏幕内容。该授权弹窗由系统统一管理,用户必须主动同意才能开始屏幕共享。

Button mStartScreenShareBtn = findViewById(R.id.start_screen_share_btn);
mStartScreenShareBtn.setOnClickListener(v -> {
    if(mAliRtcEngine != null) {
        // 配置屏幕共享流编码配置
        getScreenShareEncoderConfiguration();
        mAliRtcEngine.setScreenShareEncoderConfiguration(screenShareEncoderConfiguration);
        // 开启屏幕共享采集
        mAliRtcEngine.startScreenShare(null, screenShareMode);
    }
});

5.停止屏幕采集

调用 stopScreenShare 方法停止屏幕共享采集,结束屏幕内容的采集和推送,释放相关资源。

Button mStopScreenShareBtn = findViewById(R.id.stop_screen_share_btn);
mStopScreenShareBtn.setOnClickListener(v -> {
    if(mAliRtcEngine != null && mAliRtcEngine.isScreenSharePublished()) {
        mAliRtcEngine.stopScreenShare();
    }
});

6.(远端)观看屏幕共享

当用户开启屏幕共享后,频道内的其他用户会收到 onRemoteTrackAvailableNotify 回调通知。通过该回调,客户端能够实时感知远端用户音视频流的变化,包括摄像头视频流和屏幕共享视频流的发布或停止。应用可以根据回调参数动态创建或移除对应的渲染视图,实现对远端屏幕共享内容的展示与管理。

onRemoteTrackAvailableNotify 回调中,远端视频流的变化通过枚举 AliRtcVideoTrack 进行标识,具体包括:

  • AliRtcVideoTrackNo (0):无视频流,表示远端用户当前未推送任何视频流。

  • AliRtcVideoTrackCamera (1):仅推送摄像头视频流。

  • AliRtcVideoTrackScreen (2):仅推送屏幕共享视频流。

  • AliRtcVideoTrackBoth (3):同时推送摄像头和屏幕共享视频流。

应用需要通过判断该枚举值,动态调整 UI 显示逻辑,如分别显示摄像头画面和屏幕共享画面,或移除对应的视图。

private AliRtcEngineNotify mRtcEngineNotify = new AliRtcEngineNotify() {
    // 在onRemoteTrackAvailableNotify回调中设置远端视频流渲染控件
    @Override
    public void onRemoteTrackAvailableNotify(String uid, AliRtcEngine.AliRtcAudioTrack audioTrack, AliRtcEngine.AliRtcVideoTrack videoTrack){
        handler.post(new Runnable() {
            @Override
            public void run() {
                if(videoTrack == AliRtcVideoTrackCamera || videoTrack == AliRtcVideoTrackScreen) {
                    viewRemoteVideo(uid, videoTrack);
                } else if (videoTrack == AliRtcVideoTrackBoth) {
                    viewRemoteVideo(uid, AliRtcVideoTrackCamera);
                    viewRemoteVideo(uid, AliRtcVideoTrackScreen);
                } else if(videoTrack == AliRtcVideoTrackNo) {
                    removeAllRemoteVideo(uid);
                }
            }
        });
    }
}

/**
 * 显示远端流(包括摄像头流和屏幕共享流)
 * @param uid 远端用户 ID
 * @param videoTrack 视频流类型
 */
private void viewRemoteVideo(String uid, AliRtcEngine.AliRtcVideoTrack videoTrack) {
    String streamKey = getStreamKey(uid, videoTrack);
    FrameLayout view;
    if (remoteViews.containsKey(streamKey)) {
        view = remoteViews.get(streamKey);
        if (view != null) {
            view.removeAllViews();
        } else {
            view = createVideoView(streamKey);
            gridVideoContainer.addView(view);
            remoteViews.put(streamKey, view);
        }
    } else {
        view = createVideoView(streamKey);
        gridVideoContainer.addView(view);
        remoteViews.put(streamKey, view);
    }
    // 创建 SurfaceView 并设置渲染
    SurfaceView surfaceView = mAliRtcEngine.createRenderSurfaceView(this);
    surfaceView.setZOrderMediaOverlay(true);
    view.addView(surfaceView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    // 配置画布
    AliRtcEngine.AliRtcVideoCanvas videoCanvas = new AliRtcEngine.AliRtcVideoCanvas();
    videoCanvas.view = surfaceView;
    mAliRtcEngine.setRemoteViewConfig(videoCanvas, uid, videoTrack);
}

/**
 * 移除指定用户视频流的画面
 * @param uid 远端用户 ID
 * @param videoTrack 视频流类型
 */
private void removeRemoteVideo(String uid, AliRtcEngine.AliRtcVideoTrack videoTrack) {
    String streamKey = getStreamKey(uid, videoTrack);

    // 找到对应的 FrameLayout 容器并移除视图
    FrameLayout frameLayout = remoteViews.remove(streamKey);
    if(frameLayout != null) {
        frameLayout.removeAllViews();
        gridVideoContainer.removeView(frameLayout);
        Log.d("RemoveRemoteVideo", "Removed video stream for: " + streamKey);
    }
}
/**
 * 移除指定用户的所有视频
 * @param uid 远端用户 ID
 */
private void removeAllRemoteVideo(String uid) {
    removeRemoteVideo(uid, AliRtcVideoTrackCamera);
    removeRemoteVideo(uid, AliRtcVideoTrackScreen);
}

7.(可选)配置屏幕共享音频音量

如果需要共享系统声音,可以调用setAudioShareVolume接口控制屏幕共享音频流的音量。

mAliRtcEngine.setAudioShareVolume(60);