在视频通话或互动直播中进行屏幕共享,可以将说话人或主播的屏幕内容,以视频的方式分享给其他说话人或观众观看,以提高沟通效率。
屏幕共享在如下场景中应用广泛:
本节介绍如何使用 3.7.0 版和之后的 Android SDK 实现屏幕共享。
在实现屏幕共享前,请确保已在你的项目中实现基本的实时音视频功能。详见开始音视频通话或开始互动直播。
拷贝 SDK 中的 AgoraScreenShareExtension.aar
文件到 /app/libs/
目录下。
在 /app/build.gradle
文件的 dependencies
节点中添加如下行,以支持导入 aar 格式的文件。
implementation fileTree(dir: "libs", include: ["*.jar","*.aar"])
调用 startScreenCapture
开启屏幕共享。
我们在 GitHub 上提供开源示例项目供你参考。
屏幕共享功能目前存在一些使用限制和注意事项,同时会产生费用,声网推荐你在调用 API 前先阅读如下 API 参考:
startScreenCapture
updateScreenCaptureParameters
stopScreenCapture
本节介绍如何使用早于 3.7.0 版的 Android SDK 实现屏幕共享。
在实现屏幕共享前,请确保已在你的项目中实现基本的实时音视频功能。详见开始音视频通话或开始互动直播。
因为声网 SDK 不提供在 Android 平台实现屏幕共享的 API,所以你需要结合 Android 原生的屏幕采集 API 和声网 SDK 的自定义视频采集 API 实现该功能。
android.media.projection
和 android.hardware.display.VirtualDisplay
获取并传输屏幕数据。SurfaceView
传给 VirtualDisplay
作为桌面图像数据的接受方。SurfaceView
绘制回调中得到的图像数据作为自定义视频源,使用声网 SDK 的 Push
方式或 mediaIO
方式将视频源推送到 SDK。详见自定义视频采集与渲染。下图展示了 Android 平台实现屏幕共享的数据流转:
实现 IVideoSource
接口和 IVideoFrameConsumer
类,并对 IVideoSource
接口中的回调进行重写。
// 实现 IVideoSource 接口
public class ExternalVideoInputManager implements IVideoSource {
...
// 在初始化视频源时,从回调获取 IVideoFrameConsumer 对象
@Override
public boolean onInitialize(IVideoFrameConsumer consumer) {
mConsumer = consumer;
return true;
}
@Override
public boolean onStart() {
return true;
}
@Override
public void onStop() {
}
// 在 IVideoFrameConsumer 被 media engine 释放时,将 IVideoFrameConsumer 对象设为空
@Override
public void onDispose() {
Log.e(TAG, "SwitchExternalVideo-onDispose");
mConsumer = null;
}
@Override
public int getBufferType() {
return TEXTURE.intValue();
}
@Override
public int getCaptureType() {
return CAMERA;
}
@Override
public int getContentHint() {
return MediaIO.ContentHint.NONE.intValue();
}
...
}
// 实现 IVideoFrameConsumer 类
private volatile IVideoFrameConsumer mConsumer;
在加入频道前,设置自定义视频源。
// 对自定义视频输入线程进行设置
// 示例代码使用了 grafika 开源项目中的类。grafika 开源项目对 Android 的图形架构进行了封装。参考:https://source.android.com/devices/graphics/architecture
// EglCore 类,GlUtil 类,EGLContext 类, ProgramTextureOES 类的具体实现参考:https://github.com/google/grafika
// GLThreadContext 类包含 EglCore 类, EGLContext 类, ProgramTextureOES 类
private void prepare() {
// 通过 EglCore 类创建 OpenGL ES 环境
mEglCore = new EglCore();
mEglSurface = mEglCore.createOffscreenSurface(1, 1);
mEglCore.makeCurrent(mEglSurface);
// 通过 GlUtil 类创建 EGL texture 对象
mTextureId = GlUtil.createTextureObject(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
// 通过 EGL texture 对象创建 SurfaceTexture 对象
mSurfaceTexture = new SurfaceTexture(mTextureId);
// 通过 SurfaceTexture 对象创建 Surface 对象
mSurface = new Surface(mSurfaceTexture);
// 将 EGLCore 对象,EGL context 对象,和 ProgramTextureOES 对象传给 GLThreadContext 对象的成员
mThreadContext = new GLThreadContext();
mThreadContext.eglCore = mEglCore;
mThreadContext.context = mEglCore.getEGLContext();
mThreadContext.program = new ProgramTextureOES();
// 设置自定义视频源
ENGINE.setVideoSource(ExternalVideoInputManager.this);
}
通过 MediaProjection
创建 intent
并将 intent
传递给 startActivityForResult()
,进行屏幕图像采集。
private class VideoInputServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
mService = (IExternalVideoInputService) iBinder;
// 开始屏幕图像采集。使用 MediaProjection 的 Android 版本必须是 Lollipop 及以上。
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
// 实例化 MediaProjectionManager 对象
MediaProjectionManager mpm = (MediaProjectionManager)
getContext().getSystemService(Context.MEDIA_PROJECTION_SERVICE);
// 创建 intent
Intent intent = mpm.createScreenCaptureIntent();
// 开始屏幕采集
startActivityForResult(intent, PROJECTION_REQ_CODE);
}
通过 activity result 获取录屏信息。
// 通过 activity result 获取录屏信息的 intent
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PROJECTION_REQ_CODE && resultCode == RESULT_OK) {
...
// 设置外部视频输入为录屏信息
mService.setExternalVideoInput(ExternalVideoInputManager.TYPE_SCREEN_SHARE, data);
}
catch (RemoteException e) {
e.printStackTrace();
}
}
}
setExternalVideoInput(int type, Intent intent)
方法的实现如下:
// 根据 intent 获取录屏视频的参数
boolean setExternalVideoInput(int type, Intent intent) {
if (mCurInputType == type && mCurVideoInput != null
&& mCurVideoInput.isRunning()) {
return false;
}
IExternalVideoInput input;
switch (type) {
...
case TYPE_SCREEN_SHARE:
// 从 MediaProjection 的 intent 获取录屏视频的数据
int width = intent.getIntExtra(FLAG_SCREEN_WIDTH, DEFAULT_SCREEN_WIDTH);
int height = intent.getIntExtra(FLAG_SCREEN_HEIGHT, DEFAULT_SCREEN_HEIGHT);
int dpi = intent.getIntExtra(FLAG_SCREEN_DPI, DEFAULT_SCREEN_DPI);
int fps = intent.getIntExtra(FLAG_FRAME_RATE, DEFAULT_FRAME_RATE);
Log.i(TAG, "ScreenShare:" + width + "|" + height + "|" + dpi + "|" + fps);
// 使用录屏视频的数据实例化 ScreenShareInput 类
input = new ScreenShareInput(context, width, height, dpi, fps, intent);
break;
default:
input = null;
}
// 将视频输入设为实例化的 ScreenShareInput 类,并新建外部视频输入线程。具体实现详见示例项目。
setExternalVideoInput(input);
mCurInputType = type;
return true;
}
在外部视频输入线程的初始化过程中,通过 MediaProjection
创建 VirtualDisplay
,并把 VirtualDisplay
的内容渲染到 SurfaceView
。
public void onVideoInitialized(Surface target) {
MediaProjectionManager pm = (MediaProjectionManager)
mContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
mMediaProjection = pm.getMediaProjection(Activity.RESULT_OK, mIntent);
if (mMediaProjection == null) {
Log.e(TAG, "media projection start failed");
return;
}
// 通过 MediaProjection 创建 VirtualDisplay,并把 VirtualDisplay 的内容渲染到 SurfaceView
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
VIRTUAL_DISPLAY_NAME, mSurfaceWidth, mSurfaceHeight, mScreenDpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC, target,
null, null);
}
将 SurfaceView
作为自定义视频源。本地用户加入频道后,自采集模块通过 ExternalVideoInputThread
线程中的 consumeTextureFrame
消费视频帧,并将视频帧发送到 SDK。
public void run() {
...
// 调用 updateTexImage() 将数据更新到 OpenGL ES 纹理对象
// 调用getTransformMatrix() 转换纹理坐标
try {
mSurfaceTexture.updateTexImage();
mSurfaceTexture.getTransformMatrix(mTransform);
}
catch (Exception e) {
e.printStackTrace();
}
// 通过 onFrameAvailable 回调获取采集的视频帧信息。此处的 onFrameAvailable 为 Android 原生方法在 ScreenShareInput 类中的重写,可以获取 Texture ID,transform 信息。
// 屏幕共享无需在本地渲染视频,因为无需本地预览
if (mCurVideoInput != null) {
mCurVideoInput.onFrameAvailable(mThreadContext, mTextureId, mTransform);
}
mEglCore.makeCurrent(mEglSurface);
GLES20.glViewport(0, 0, mVideoWidth, mVideoHeight);
if (mConsumer != null) {
Log.e(TAG, "SDK encoding->width:" + mVideoWidth + ",height:" + mVideoHeight);
// 调用 consumeTextureFrame 消费视频帧并发送到 SDK
mConsumer.consumeTextureFrame(mTextureId,
TEXTURE_OES.intValue(),
mVideoWidth, mVideoHeight, 0,
System.currentTimeMillis(), mTransform);
}
// 等待下一帧
waitForNextFrame();
...
}
我们在 GitHub 上提供开源示例项目,以展示如何完成屏幕共享与摄像头切换。
目前声网 RTC Native SDK 只支持每个 app 创建一个 RtcEngine
实例。如果你要同时发送屏幕共享和本地采集的视频,需要通过多进程实现。
你需要分别为屏幕共享发流和摄像头采集发流创建独立的进程。两个进程分别通过以下方法将视频数据发送给 SDK:
joinChannel
方式实现。此进程可作为主进程,并通过 AIDL (Android Interface Definition Language) 与屏幕共享发流进程进行通信。你可以参考 Android 官方文档了解 AIDL。MediaProjection
、VirtualDisplay
和自定义视频采集方式实现。屏幕共享进程会单独创建一个 RtcEngine
对象,并通过此对象创建和加入一个用于屏幕共享发流的频道。muteAllRemoteAudioStreams(true)
和 muteAllRemoteVideoStreams(true)
停止接收远端的音视频流。下面的示例代码使用多进程发送屏幕共享和本地采集的视频。进程之间使用 AIDL 进行通信。
在 AndroidManifest.xml
中为组件设置 android:process
。
<application>
<activity
android:name=".impl.ScreenCapture$ScreenCaptureAssistantActivity"
android:process=":screensharingsvc"
android:screenOrientation="fullUser"
android:theme="@android:style/Theme.Translucent" />
<service
android:name=".impl.ScreenSharingService"
android:process=":screensharingsvc">
<intent-filter>
<action android:name="android.intent.action.screenshare" />
</intent-filter>
</service>
</application>
创建 AIDL 接口,接口中包含用于进程间通信的方法。
// 包含控制屏幕共享进程的方法
// IScreenSharing.aidl
package io.agora.rtc.ss.aidl;
import io.agora.rtc.ss.aidl.INotification;
interface IScreenSharing {
void registerCallback(INotification callback);
void unregisterCallback(INotification callback);
void startShare();
void stopShare();
void renewToken(String token);
}
// 包含接收屏幕共享进程的回调
// INotification.aidl
package io.agora.rtc.ss.aidl;
interface INotification {
void onError(int error);
void onTokenWillExpire();
}
实现屏幕共享发流进程。屏幕共享功能通过 MediaProjection
、VirtualDisplay
和自定义视频采集方式实现。屏幕共享进程会单独创建一个 RtcEngine
对象,并通过此对象创建和加入一个用于屏幕共享发流的频道。
// 定义 ScreenSharingClient 对象
public class ScreenSharingClient {
private static final String TAG = ScreenSharingClient.class.getSimpleName();
private static IScreenSharing mScreenShareSvc;
private IStateListener mStateListener;
private static volatile ScreenSharingClient mInstance;
public static ScreenSharingClient getInstance() {
if (mInstance == null) {
synchronized (ScreenSharingClient.class) {
if (mInstance == null) {
mInstance = new ScreenSharingClient();
}
}
}
return mInstance;
}
// 开始屏幕共享
public void start(Context context, String appId, String token, String channelName, int uid, VideoEncoderConfiguration vec) {
if (mScreenShareSvc == null) {
Intent intent = new Intent(context, ScreenSharingService.class);
intent.putExtra(Constant.APP_ID, appId);
intent.putExtra(Constant.ACCESS_TOKEN, token);
intent.putExtra(Constant.CHANNEL_NAME, channelName);
intent.putExtra(Constant.UID, uid);
intent.putExtra(Constant.WIDTH, vec.dimensions.width);
intent.putExtra(Constant.HEIGHT, vec.dimensions.height);
intent.putExtra(Constant.FRAME_RATE, vec.frameRate);
intent.putExtra(Constant.BITRATE, vec.bitrate);
intent.putExtra(Constant.ORIENTATION_MODE, vec.orientationMode.getValue());
context.bindService(intent, mScreenShareConn, Context.BIND_AUTO_CREATE);
} else {
try {
mScreenShareSvc.startShare();
} catch (RemoteException e) {
e.printStackTrace();
Log.e(TAG, Log.getStackTraceString(e));
}
}
}
// 停止屏幕共享
public void stop(Context context) {
if (mScreenShareSvc != null) {
try {
mScreenShareSvc.stopShare();
mScreenShareSvc.unregisterCallback(mNotification);
} catch (RemoteException e) {
e.printStackTrace();
Log.e(TAG, Log.getStackTraceString(e));
} finally {
mScreenShareSvc = null;
}
}
context.unbindService(mScreenShareConn);
}
...
}
bind 屏幕共享 service 之后,屏幕共享进程会创建一个 RtcEngine
对象,并加入用于屏幕共享发流的频道。
@Override
public IBinder onBind(Intent intent) {
// 创建一个 RtcEngine 对象
setUpEngine(intent);
// 设置视频编码属性
setUpVideoConfig(intent);
// 加入频道
joinChannel(intent);
return mBinder;
}
屏幕共享进程创建 RtcEngine
对象时,需要进行以下设置:
// 取消订阅所有音频流
mRtcEngine.muteAllRemoteAudioStreams(true);
// 取消订阅所有视频流
mRtcEngine.muteAllRemoteVideoStreams(true);
// 关闭音频模块
mRtcEngine.disableAudio();
实现摄像头本地采集发流进程以及屏幕共享进程的触发机制。
public class MultiProcess extends BaseFragment implements View.OnClickListener
{
private static final String TAG = MultiProcess.class.getSimpleName();
// 定义用于屏幕共享进程的 uid
private static final Integer SCREEN_SHARE_UID = 10000;
...
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState)
{
super.onActivityCreated(savedInstanceState);
Context context = getContext();
if (context == null)
{
return;
}
try
{
// 创建 RtcEngine 实例
engine = RtcEngine.create(context.getApplicationContext(), getString(R.string.agora_app_id), iRtcEngineEventHandler);
// 初始化屏幕共享进程
mSSClient = ScreenSharingClient.getInstance();
mSSClient.setListener(mListener);
}
catch (Exception e)
{
e.printStackTrace();
getActivity().onBackPressed();
}
}
...
// 执行屏幕共享进程,将 App ID,channel ID 等信息发送给屏幕共享进程
else if (v.getId() == R.id.screenShare){
String channelId = et_channel.getText().toString();
if (!isSharing) {
mSSClient.start(getContext(), getResources().getString(R.string.agora_app_id), null,
channelId, SCREEN_SHARE_UID, new VideoEncoderConfiguration(
VD_640x360,
FRAME_RATE_FPS_15,
STANDARD_BITRATE,
ORIENTATION_MODE_ADAPTIVE
));
screenShare.setText(getResources().getString(R.string.stop));
isSharing = true;
} else {
mSSClient.stop(getContext());
screenShare.setText(getResources().getString(R.string.screenshare));
isSharing = false;
}
}
...
// 创建本地预览视图
SurfaceView surfaceView = RtcEngine.CreateRendererView(context);
if(fl_local.getChildCount() > 0)
{
fl_local.removeAllViews();
}
...
// 加入频道
int res = engine.joinChannel(accessToken, channelId, "Extra Optional Data", 0);
if (res != 0)
{
showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
return;
}
join.setEnabled(false);
}
我们在 GitHub 上提供开源示例项目,以展示如何通过双进程同时发布屏幕共享流和摄像头采集流。