本文将为您介绍iOS端如何实现屏幕共享。
功能介绍
屏幕共享功能允许用户在视频通话、直播过程中将自己的屏幕内容实时分享给频道内的其他用户,实现信息的即时共享与可视化交流。
技术原理
iOS 系统不支持在主进程进行屏幕采集,因此实现屏幕共享功能需要增加Broadcast Upload Extension
录屏进程以配合主进程进行推流,录屏进程由系统在需要录屏的时候创建,使用 iOS 原生的 ReplayKit 框架实现屏幕录制,然后将屏幕共享流传递给主进程进行推流。
由于苹果隐私设置,不同 App、不同进程之间数据无法互通,因此需要使用苹果提供的 App Group 机制,并为主进程和录屏进程配置相同的 App Group ID。
示例项目
ARTC 提供了开源示例项目供您参考,您可以前往下载或查看其中的代码示例代码:iOS实现屏幕共享。
前提条件
在实现屏幕共享前,请确保达成以下条件:
注意事项
该功能对设备性能要求较高,我们推荐你使用 iPhone X 及以上设备。
用户在 iOS 设备上开启屏幕共享后,因系统限制,音频路由会自动切换为听筒。
项目配置
(可选)创建 App Group
虽然在未配置 App Group(使用未注册的 ID)的情况下,屏幕共享功能在部分场景下仍可运行,但这种做法缺乏稳定性保障,可能导致扩展进程无法访问共享数据或出现不可预期的行为。
为确保主 App 与录屏扩展之间的数据同步与通信可靠,建议始终配置并使用一个合法、注册在 Apple Developer Portal 中的 App Group ID。
注册 App Group 请参考https://developer.apple.com/cn/help/account/identifiers/register-an-app-group。
创建 Broadcast Upload Extension
使用 Xcode 打开您的项目,依次点击File -> New -> Target... ,在弹出的窗口中选择 iOS 平台,然后选择Broadcast Upload Extension,之后点击 Next。
在弹出的界面设置 Product Name 等信息,语言选择 Objective-C,取消勾选 Include UI Extension。之后,点击 Finish,Xcode 会自动创建对应 Extension 的文件夹,其中包含 SampleHandler 等文件。
集成 AliScreenShare.framework:
使用 CocoaPods 工具自动集成
打开终端,在您的开发设备上安装 CocoaPods 工具,如果您已经完成安装,可以跳过此步骤。
sudo gem install cocoapods
打开终端,进入项目根目录,在终端窗口中输入以下命令,创建 Podfile 文件。
pod init
打开并编辑生成的 Podfile文件,为上面创建的
ScreenShareExtension
录屏进程添加 AliScreenShare 依赖。
# Uncomment the next line to define a global platform for your project platform :ios, '11.0' target 'ARTCExample' do # Pods for ARTCExample pod 'AliVCSDK_ARTC', '~> 7.5.0' end # 为ScreenShareExtension录屏进程添加AliScreenShare库依赖 target 'ScreenShareExtension' do # Pods for ScreenShareExtension pod "AliScreenShare", '7.5.0' end
在终端窗口中输入以下命令更新项目中的CocoaPods依赖库。
pod install
成功执行后,项目文件夹下将生成一个后缀为.xcworkspace后缀的工程文件,双击该文件通过 Xcode打开项目即可加载工作区自动集成 CocoaPods 依赖。
手动集成
点击下载SDK,将下载的AliScreenShare.framework 文件拷贝到您的项目目录。选择刚才创建的 Extension 工程。选择录屏进程的 Extension Target,依次选择 General -> Frameworks and Libraries,点击加号,然后点击 Add Files,选择 AliScreenShare.framework 导入。
为主项目 Tartget 与 Extension Target 配置相同的 App Groups:依次选择 Target -> Signing & Capabilities,设置 App Groups,如果没有则创建。录屏扩展应用和主应用位于不同的进程中,并且拥有不同的沙盒,为了实现跨进程通信,需要使用苹果的 App Group 功能。
如下图需要为两个 Target 配置相同的 App Group ID。
选中 Extension 的 Target,点击 General,设置支持的 iOS 版本(Minimum Deployments)为 12.0 以上。
主 App 和 App Extension 需要保持相同的 Bundle Id 前缀, 并使用同样的开发证书进行签名。
在新创建的 Target 中,Xcode 会自动创建一个名为 "SampleHandler" 的类,用如下代码进行替换其中的.m文件。
说明需将代码中的 APP GROUP ID(kAppGroup)改为上文中的创建的 App Group Identifier。
#import "SampleHandler.h"
#import <AliScreenShare/AliScreenShareExt.h>
static NSString * _Nonnull kAppGroup = @"group.com.aliyun.video"; // 屏幕共享主app和插件的AppGroup
@interface SampleHandler() <AliScreenShareExtDelegate>
@property (nonatomic, assign) int32_t frameNum;
@end
@implementation SampleHandler
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
// User has requested to start the broadcast. Setup info from the UI extension can be supplied but optional.
NSLog(@"SampleHandler SEND broadcastStartedWithSetupInfo");
[[AliScreenShareExt sharedInstance] setupWithAppGroup:kAppGroup delegate:self];
}
- (void)broadcastPaused {
// User has requested to pause the broadcast. Samples will stop being delivered.
NSLog(@"SampleHandler SEND broadcastPaused");
}
- (void)broadcastResumed {
// User has requested to resume the broadcast. Samples delivery will resume.
NSLog(@"SampleHandler SEND broadcastResumed");
}
- (void)broadcastFinished {
// User has requested to finish the broadcast.
NSLog(@"SampleHandler SEND broadcastFinished");
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
@autoreleasepool {
[[AliScreenShareExt sharedInstance] sendSampleBuffer:sampleBuffer type:sampleBufferType];
}
}
#pragma mark - AliScreenShareExtDelegate
- (void)finishBroadcastWithError:(AliScreenShareExt *)broadcast error:(NSError *)error
{
[self finishBroadcastWithError:error];
}
@end
修改
ARTCExample/BasicUsage/ScreenShare/ScreenShareVC.swift
文件中的 kAppGroup 参数为您设置的。
功能实现
1.相机流和屏幕共享流配置
ARTC 支持推送相机流和屏幕共享流,请根据业务场景灵活配置。
1.1.仅需推送屏幕共享流
如果您的场景中如果仅需要推送屏幕流,由于 SDK 默认会默认推送相机流,因此需要您主动调用publishLocalVideoStream
关闭相机流推送,流程如下:
加入频道前调用
publishLocalVideoStream(false)
来关闭相机流推送。加入频道后调用
startScreenShare
开启屏幕录制并推送屏幕共享流。
engine.publishLocalVideoStream(false)
1.2.需要同时推送屏幕共享流和相机流
如果您的场景中需要同时推送相机流和屏幕共享流,需要:
调用
publishLocalVideoStream(true)
开启相机流推送。(默认行为,可省略)。加入频道后调用
startScreenShare
开启屏幕录制并推送屏幕共享流。
engine.publishLocalVideoStream(true)
2.(可选)配置屏幕采集编码参数
如果需要自定义屏幕共享视频流的编码属性,可以调用setScreenShareEncoderConfiguration
接口进行配置,包含分辨率、帧率、码率、GOP、视频旋转方向。
该接口在加入频道前后均可配置,如果每次入会只需要设置一次屏幕流视频编码属性,建议在入会前调用。
如果需要更新配置,可以多次调用该接口。
相关配置如下:
参数名 | 参数说明 | 默认值 |
dimensions | 视频分辨率。 | 0x0,表示推流分辨率跟随屏幕采集的分辨率。最大取值为3840x2160。 |
frameRate | 视频帧率。 | 默认帧率为 5,最大取值为 30。 |
bitrate | 视频编码码率(Kbps)。 注意:码率设置根据分辨率和帧率有对应的合理范围,该值设置在合理范围内有效,否则SDK会自动调节码率到有效值。 | 512 |
keyFrameInterval | 关键帧间隔,GOP。单位为毫秒(ms)。 | 默认值0,表示SDK内部控制关键帧间隔。 |
forceStrictKeyFrameInterval | 是否强制编码器严格按照设置的关键帧间隔产生关键帧。 | 默认值为 false。
|
rotationMode | 推流旋转。 | 默认值为AliRtcRotationMode_0。可选择 0、90、180、270 四个角度。 |
示例代码如下:
var screenShareConfig: AliRtcScreenShareEncoderConfiguration = AliRtcScreenShareEncoderConfiguration()
screenShareConfig.dimensions = CGSize(width: Int(720), height: Int(1280))
screenShareConfig.frameRate = 15
screenShareConfig.bitrate = 512
screenShareConfig.keyFrameInterval = 2000
alirtcEngine.setScreenShareEncoderConfiguration(screenShareConfig)
3.开启屏幕采集
调用
startScreenShare
开启屏幕采集,并根据你的应用场景进行参数设置:kAppGroup: 屏幕采集插件和主app约定使用相同的 App Group。
mode:屏幕共享模式,包含不共享、仅共享屏幕视频、仅共享系统音频、共享系统音频和屏幕视频。
// 主App需要与Extension进程使用相同的App Group
let kAppGroup = "group.com.aliyun.video";
@IBAction func onStartScreenShareBtnClicked(_ sender: UIButton) {
guard let alirtcEngine = self.rtcEngine else {return}
// 配置屏幕共享编码参数
alirtcEngine.setScreenShareEncoderConfiguration(screenShareConfig)
// 启动弹窗
startBroadcastPicker()
// 开始屏幕共享
alirtcEngine.startScreenShare(kAppGroup, mode: screenShareMode)
}
启用 Extension 扩展进程。
Apple 需要用户主动操作才能开启屏幕采集,下面的startBoardcastPicker
是利用 Apple 在 iOS 12.0 引入的RPSystemBroadcastPickerView
,主要用于弹出“直播屏幕”的弹窗,提示用户操作开启屏幕录制。
// 创建并配置一个系统广播选择器视图,苹果要求屏幕共享需要用户显示触发
func startBroadcastPicker() {
if #available(iOS 12.0, *) {
let broadcastPickerView = RPSystemBroadcastPickerView(frame: CGRect(x: 0, y: 0, width: 44, height: 44))
guard let bundlePath = Bundle.main.path(forResource: "ScreenShareExtension", ofType: "appex", inDirectory: "PlugIns") else {
self.showErrorAlertView("Can not find bundle at path", code: 0, forceShow: false)
return
}
guard let bundle = Bundle(path: bundlePath) else {
self.showErrorAlertView("Can not find bundle at path", code: 0, forceShow: false)
return
}
broadcastPickerView.preferredExtension = bundle.bundleIdentifier
// Traverse the subviews to find the button to skip the step of clicking the system view.
// This solution is not officially recommended by Apple, and may be invalid in future system updates.
for subView in broadcastPickerView.subviews {
if let button = subView as? UIButton {
button.sendActions(for: .allEvents)
}
}
} else {
self.showErrorAlertView("This feature only supports iOS 12 or above", code: 0, forceShow: false)
return
}
}
4.停止屏幕共享
调用stopScreenShare
,在频道内停止屏幕共享,示例如下:
@IBAction func onStopScreenShareBtnClicked(_ sender: UIButton) {
guard let alirtcEnging = self.rtcEngine else {return}
if(alirtcEnging.isScreenSharePublished()) {
alirtcEnging.stopScreenShare()
}
}
5.(远端)观看屏幕共享画面
ARTC 允许同时推拉屏幕流和相机流,当远端用户开始推送音视频流时,本端会触发onRemoteTrackAvailableNotify
回调,通过 videoTrack 的状态变化来表示远端用户视频流信息,可以根据状态变化来部署用户的画面。
// 保存所有视图
var videoSeatViewMap: [String: SeatView] = [:]
// 根据回调部署画面
func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) {
"onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack) videoTrack: \(videoTrack)".printLog()
// 远端用户的流状态
DispatchQueue.main.async {
switch videoTrack {
case .no:
// 移除所有该用户的视图
self.removeSeatView(uid: uid, streamType: .camera)
self.removeSeatView(uid: uid, streamType: .screen)
case .camera:
// 添加相机视图
self.createOrUpdateSeatView(uid: uid, streamType: .camera)
case .screen:
// 添加屏幕共享视图
self.createOrUpdateSeatView(uid: uid, streamType: .screen)
case .both:
// 添加相机视图和屏幕共享视图
self.createOrUpdateSeatView(uid: uid, streamType: .camera)
self.createOrUpdateSeatView(uid: uid, streamType: .screen)
@unknown default:
break
}
}
}
// 移除指定用户的指定画面
func removeSeatView(uid: String, streamType: StreamType) {
let key = "\(uid)_\(streamType)"
guard let seatView = videoSeatViewMap.removeValue(forKey: key) else { return }
// 1. 从UI中移除
seatView.removeFromSuperview()
// 2. 清理视频资源
rtcEngine?.setRemoteViewConfig(nil, uid: uid, for: streamType == .camera ? .camera : .screen)
// 3. 检查是否还有该用户的其他视图
let hasOtherViews = videoSeatViewMap.keys.contains { $0.hasPrefix("\(uid)_") }
if !hasOtherViews {
// 移除用户容器
if let container = findUserContainer(for: uid) {
container.removeFromSuperview()
}
} else {
// 重新布局剩余视图
updateLayoutForUser(uid: uid)
}
}
// 创建或更新一个视频通话渲染视图,并加入到contentScrollView中
func createOrUpdateSeatView(uid: String, streamType: StreamType) ->SeatView {
let key = "\(uid)_\(streamType)"
// 1. 如果已有视图,直接返回
if let existingView = videoSeatViewMap[key] {
return existingView
}
// 2. 创建新视图
let seatView = SeatView(frame: .zero)
seatView.seatInfo = SeatInfo(uid: uid, streamType: streamType)
if uid != self.userId {
// 3. 配置视频画布
let canvas = AliVideoCanvas()
canvas.view = seatView.canvasView
canvas.renderMode = .fill
canvas.mirrorMode = streamType == .screen ? .allDisabled : .allEnabled
canvas.rotationMode = ._0
rtcEngine?.setRemoteViewConfig(canvas, uid: uid, for: streamType == .camera ? .camera : .screen)
}
// 4. 添加到管理字典
videoSeatViewMap[key] = seatView
// 6. 更新布局
updateLayoutForUser(uid: uid)
return seatView
}
6.(可选)配置屏幕共享音频音量
如果需要共享系统声音,可以调用setAudioShareAppVolume
接口控制屏幕共享音频流的音量。
@IBAction func onShareAudioVolumeSliderChanged(_ sender: UISlider) {
let volume = Int32(sender.value)
rtcEngine?.setAudioShareAppVolume(volume)
}