屏幕共享

在 iOS 平台下支持两种不同的屏幕分享方案:
应用内分享
即只能分享当前 App 的画面,该特性需要 iOS 13 及以上版本的操作系统才能支持。由于无法分享当前 App 之外的屏幕内容,因此适用于对隐私保护要求高的场景。
跨应用分享
基于苹果的 Replaykit 方案,能够分享整个系统的屏幕内容,但需要当前 App 额外提供一个 Extension 扩展组件,因此对接步骤也相对应用内分享要多一点。

支持的平台

iOS
Android
macOS
Windows
Electron
Chrome 浏览器

应用内分享

应用内分享的方案非常简单,只需要调用 RoomEngine 提供的接口 startScreenCapture 并传入参数appgroup 即可。
同时您可以调用updateVideoQualityEx修改编码参数,我们推荐的用于 iOS 屏幕分享的编码参数是:
参数项
参数名称
常规推荐值
文字教学场景
分辨率
videoResolution
1280 × 720
1920 × 1080
帧率
fps
10 FPS
8 FPS
最高码率
bitrate
1600 kbps
2000 kbps
由于屏幕分享的内容一般不会剧烈变动,所以设置较高的 FPS 并不经济,推荐10 FPS即可。
如果您需要更高的画质,请调高FPS。
如果您要分享的屏幕内容包含大量文字,可以适当提高分辨率和码率设置。
最高码率(videoBitrate)是指画面在剧烈变化时的最高输出码率,如果屏幕内容变化较少,实际编码码率会比较低。
示例:
import RTCRoomEngine

let roomEngine = TUIRoomEngine.sharedInstance()

roomEngine.startScreenCapture(appGroup: "")

let params = TUIRoomVideoEncoderParams()
params.fps = 10 //替换为您真实需要的值
params.resolutionMode = .portrait //竖屏分辨率
params.bitrate = .1600 //此处换为您真实需要的值
params.videoResolution = .quality720P //此处换位您需要的值

roomEngine.updateVideoQualityEx(streamType: .screenStream, params: params)

跨应用分享

iOS 系统上的跨应用屏幕分享,需要增加 Broadcast Upload Extension 录屏进程以配合主 App 进程进行推流。Extension 录屏进程由系统在需要录屏的时候创建,并负责接收系统采集到屏幕图像。因此需要:
1. 创建 App Group,并在 XCode 中进行配置(可选)。这一步的目的是让 Extension 录屏进程可以同主 App 进程进行跨进程通信。
2. 在您的工程中,新建一个 Broadcast Upload Extension 的 Target,并在其中集成 SDK 压缩包中专门为扩展模块定制的 TXLiteAVSDK_ReplayKitExt.framework
3. 对接主 App 端的接收逻辑,让主 App 等待来自 Broadcast Upload Extension 的录屏数据。
注意:
如果跳过第1步,也就是不配置 App Group(接口传 nil),屏幕分享依然可以运行,但稳定性要打折扣,故虽然步骤较多,但请尽量配置正确的 App Group 以保障屏幕分享功能的稳定性。

步骤1:创建 App Group

使用您的账号登录 Apple 开发者官方网站 ,进行以下操作,注意完成后需要重新下载对应的 Provisioning Profile
1. 单击 Certificates, IDs & Profiles
2. 在右侧的界面中单击加号。
3. 选择 App Groups,单击 Continue
4. 在弹出的表单中填写 Description 和 Identifier,其中 Identifier 需要传入接口中的对应的 AppGroup 参数。完成后单击 Continue



5. 回到 Identifier 页面,左上边的菜单中选择 App IDs,然后单击您的 App ID(主 App 与 Extension 的 AppID 需要进行同样的配置)。
6. 选中 App Groups 并单击 Edit
7. 在弹出的表单中选择您之前创建的 App Group,单击 Continue 返回编辑页,单击 Save 保存。



8. 重新下载 Provisioning Profile 并配置到 XCode 中。

步骤2:创建 Broadcast Upload Extension

1. 在 Xcode 菜单依次单击 FileNewTarget...,选择 Broadcast Upload Extension
2. 在弹出的对话框中填写相关信息,不用勾选 Include UI Extension,单击 Finish 完成创建。
3. 将下载到的 SDK 压缩包中的 TXLiteAVSDK_ReplayKitExt.framework 拖动到工程中,勾选刚创建的 Target。
4. 选中新增加的 Target,依次单击 + Capability,双击 App Groups,如下图:

AddCapability


操作完成后,会在文件列表中生成一个名为 Target名.entitlements 的文件,如下图所示,选中该文件并单击 + 号填写上述步骤中的 App Group 即可。

AddGroup


5. 选中主 App 的 Target ,并按照上述步骤对主 App 的 Target 做同样的处理。
6. 在新创建的 Target 中,Xcode 会自动创建一个名为 "SampleHandler.h" 的文件,用如下代码进行替换。需将代码中的 APPGROUP 改为上文中的创建的 App Group Identifier
import ReplayKit
import TXLiteAVSDK_ReplayKitExt

let APPGROUP = ""

class SampleHandler: RPBroadcastSampleHandler, TXReplayKitExtDelegate {

let recordScreenKey = Notification.Name.init("TRTCRecordScreenKey")

override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
if let setupInfo = setupInfo {
}
TXReplayKitExt.sharedInstance().setup(withAppGroup: APPGROUP, delegate: self)
}
override func broadcastPaused() {
// 可以添加暂停时的资源释放或状态保存逻辑
}

override func broadcastResumed() {
// 可以添加恢复时的资源重新初始化逻辑
}

override func broadcastFinished(){
// User has requested to finish the broadcast.
TXReplayKitExt.sharedInstance().broadcastFinished()
}

func broadcastFinished(_ broadcast: TXReplayKitExt, reason: TXReplayKitExtReason) {
var tip = ""
switch reason {
case TXReplayKitExtReason.requestedByMain:
tip = "屏幕共享已结束"
break
case TXReplayKitExtReason.disconnected:
tip = "应用断开"
break
case TXReplayKitExtReason.versionMismatch:
tip = "集成错误(SDK 版本号不相符合)"
break
default:
break
}

let error = NSError(domain: NSStringFromClass(self.classForCoder), code: 0, userInfo: [NSLocalizedFailureReasonErrorKey:tip])
finishBroadcastWithError(error)
}

override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case RPSampleBufferType.video:
TXReplayKitExt
.sharedInstance()
.send(sampleBuffer, with: .video)
break
case RPSampleBufferType.audioApp:
// 可以在此处处理应用音频
break
case RPSampleBufferType.audioMic:
// 可以在此处处理麦克风音频
break
@unknown default:
let error = "未知的采样缓冲区类型: \(sampleBufferType)"
finishBroadcastWithError(NSError(domain: error, code: -1, userInfo: nil))
}
}
}

步骤3:对接主 App 端的接收逻辑

按照如下步骤,对接主 App 端的接收逻辑。也就是在用户触发屏幕分享之前,要让主 App 处于“等待”状态,以便随时接收来自 Broadcast Upload Extension 进程的录屏数据。
1. 调用 startScreenCapture 方法,并传入 步骤1 中设置的 AppGroup,让 SDK 进入“等待”状态。
2. 等待用户触发屏幕分享。如果不实现 步骤4 中的“触发按钮”,屏幕分享就需要用户在 iOS 系统的控制中心,通过长按录屏按钮来触发,这一操作步骤如下图所示:
3. 通过调用 stopScreenCapture 接口可以随时中止屏幕分享。
示例:
import RTCRoomEngine

TUIRoomEngine.sharedInstance().startScreenCapture(appGroup: "")
TUIRoomEngine.sharedInstance().stopScreenCapture()

步骤4:增加屏幕分享的触发按钮(可选)

截止到 步骤3 ,我们的屏幕分享还必须要用户从控制中心中长按录屏按钮来手动启动。您可通过下述方法实现类似腾讯会议的单击按钮即可触发的效果:
1. TRTCBroadcastExtensionLauncher 文件实现了唤起屏幕分享,将其加入到您的工程中。
2. 在您的界面上放置一个按钮,并在按钮的响应函数中调用 TRTCBroadcastExtensionLauncher 中的 launch 函数,就可以唤起屏幕分享功能了。
示例:
@objc private func buttonTapped() {
TRTCBroadcastExtensionLauncher.sharedInstance.launch()
}
import UIKit
import TXLiteAVSDK_ReplayKitExt

class TRTCBroadcastExtensionLauncher: NSObject {

var systemBroacastExtensionPicker = RPSystemBroadcastPickerView()
var prevLaunchEventTime : CFTimeInterval = 0

static let sharedInstance = TRTCBroadcastExtensionLauncher()

override init() {
super.init()
let picker = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
picker.showsMicrophoneButton = false
picker.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin]
systemBroacastExtensionPicker = picker

if let pluginPath = Bundle.main.builtInPlugInsPath,
let contents = try? FileManager.default.contentsOfDirectory(atPath: pluginPath) {

for content in contents where content.hasSuffix(".appex") {
guard let bundle = Bundle(path: URL(fileURLWithPath: pluginPath).appendingPathComponent(content).path),
let identifier : String = (bundle.infoDictionary?["NSExtension"] as? [String:Any])? ["NSExtensionPointIdentifier"] as? String
else {
continue
}
if identifier == "com.apple.broadcast-services-upload" {
picker.preferredExtension = bundle.bundleIdentifier
break
}
}
}
}

static func launch() {
TRTCBroadcastExtensionLauncher.sharedInstance.launch()
}

func launch() {
// The pop-up on iOS 12 is slow and will crash if you click quickly.
let now = CFAbsoluteTimeGetCurrent()
if now - prevLaunchEventTime < 1.0 {
return
}
prevLaunchEventTime = now

for view in systemBroacastExtensionPicker.subviews {
if let button = view as? UIButton {
button.sendActions(for: .allTouchEvents)
break
}
}
}
}

注意:
苹果在 iOS 12.0 中增加了 RPSystemBroadcastPickerView 可以从应用中弹出启动器供用户确认启动屏幕分享,到目前为止, RPSystemBroadcastPickerView 尚不支持自定义界面,也没有官方的唤起方法。
TRTCBroadcastExtensionLauncher 的原理就是遍历 RPSystemBroadcastPickerView 的子 View 寻找 UIButton 并触发了其点击事件。
但该方案不被苹果官方推荐,并可能在新一轮的系统更新中失效,因此 步骤4 只是一个可选方案,您需要自行承担风险来选用此方案。

步骤5:混流模式下屏幕共享

当您完成了上述逻辑之后,您需要调用 setLiveStreamLayoutInfo API 修改画面布局。
import RTCRoomEngine

let roomEngine = TUIRoomEngine.sharedInstance()
let layoutManager = roomEngine.getExtension(extensionType: .liveLayoutManager) as? TUILiveLayoutManager
roomEngine.startScreenCapture(appGroup: "")//替换为真实的appGroup

layoutManager.setLiveStreamLayoutInfo(
roomID: roomInfo.roomId,
layoutInfo: "" //详见下文
onSuccess: {
//修改成功
},
onError: {code ,message in
}
)
//layoutInfo格式需求:
{
"RoomId": "live_adams", //替换为您的RoomID
//当layouttype是1000时候,修改VideoEncode时候需要同步修改LayoutInfo,九宫格模式下会默认自动根据分辨率修改更新layoutInfo
"VideoEncode": {
"Width": 1080,
"Height": 1920,
},
"LayoutMode": 1000, // 0~9 内置布局模板, 1000自定义布局, 只有1000时候才能修改LayoutInfo,0为九宫格
"LayoutInfo":{
"LayoutList": [
{
"LocationX": 0, // 具体数值
"LocationY": 0, // 具体数值
"ImageWidth": 1080, // 具体数值
"ImageHeight": 960, // 具体数值
"ZOrder": 0, // 层级
"StreamType": 0, // 0为摄像头, 1为屏幕共享
"Member_Account": "admin001", //替换为您的userId
"BackgroundImageUrl": "ImageUrl",
"RoomId":"live_adams", //替换为您的RoomId
"BackgroundColor":"0x1F212C"
},
{
"LocationX": 0, // 具体数值
"LocationY": 960, // 具体数值
"ImageWidth": 1080, // 具体数值
"ImageHeight": 960, // 具体数值
"ZOrder": 0, // 层级
"StreamType": 0, // 0为摄像头, 1为屏幕共享
"Member_Account": "admin001", //替换为您的UserId
"BackgroundImageUrl": "ImageUrl",
"RoomId":"live_adams", //替换为您的RoomId
"BackgroundColor":"0x1F212C"
}
],
"MaxUserLayout": {
"LocationX": 0, // 具体数值
"LocationY": 0, // 具体数值
"ImageWidth": 1080, // 具体数值
"ImageHeight": 1920, // 具体数值
"ZOrder": 0, //层级
"StreamType": 0, // 0为摄像头, 1为屏幕共享
"Member_Account": "admin001", //替换为您的userId
"BackgroundImageUrl": "ImageUrl",
"RoomId":"live_adams", //替换为您的roomId
"BackgroundColor":"0x1F212C"
}
}

步骤6:混流模式下停止分享

当您想要停止屏幕共享并且将画面布局修改回来时,您需要调用 stopScreenCapture 接口关闭并且调用 setLiveStreamLayoutInfo 接口恢复画面布局。
import RTCRoomEngine

let roomEngine = TUIRoomEngine.sharedInstance()
let layoutManager = roomEngine.getExtension(extensionType: .liveLayoutManager) as? TUILiveLayoutManager
roomEngine.stopScreenCapture()

layoutManager.setLiveStreamLayoutInfo(
roomID: roomInfo.roomId,
layoutInfo: "",//详见下文
onSuccess: {},
onError: { code ,message in
}
)
//当您想要恢复默认的宫格布局时,您需要传入的layoutInfo参数如下:
{
"RoomId": "live_12121", //替换为真实房间号
"VideoEncode": {
"Width": 1080,
"Height": 1920
},
"LayoutMode": 0
}

观看屏幕分享

当有用户播放屏幕共享时您可以通过监听 TUIRoomObserver 中的 onUserVideoStateChanged 来感知到屏幕共享流。
示例:
import RTCRoomEngine

extension ScreenSharedController: TUIRoomObserver { //替换为您具体的业务类,此处仅为举例
func onUserVideoStateChanged(userId: String, streamType: TUIVideoStreamType, hasVideo: Bool, reason: TUIChangeReason) {
}
}