在视频通话或互动直播中进行屏幕共享,可以将说话人或主播的屏幕内容,以视频的方式分享给其他说话人或观众观看,以提高沟通效率。
屏幕共享在如下场景中应用广泛:
iOS 端的屏幕共享是通过在 Extension 中使用 iOS 原生的 ReplayKit 框架实现录制屏幕,然后将屏幕共享流作为一个用户加入频道实现的。由于 Apple 不支持在主 app 进程采集屏幕,因此你需要为屏幕共享流单独创建一个 Extension。
本节介绍如何使用 3.7.0 版和之后的 iOS SDK 实现屏幕共享。
在实现屏幕共享前,请确保已在你的项目中实现基本的实时音视频功能。详见开始音视频通话或开始互动直播。
确保已将 AgoraReplayKitExtension.xcframework
集成到项目中。该动态库封装了如下功能:
使用 Apple ReplayKit 进行屏幕录制。
使用 SDK 自采集能获取系统录屏数据,并发送给频道中其他用户。
创建一个 Broadcast Upload Extension 用于开启屏幕共享的进程:
在 Xcode 点击 File > New > Target..., 在弹出的窗口中选择 Broadcast Upload Extension, 点击 Next。
在弹出的窗口中填写 Product Name 等信息,取消勾选 Include UI Extension,点击 Finish。Xcode 会自动创建该 Extension 的文件夹,其中包含 SampleHandler.h
文件。
修改 SampleHandler.h
文件,以修改实现屏幕共享的代码逻辑:
如果你只需使用声网提供的 AgoraReplayKitExtension.xcframework
中的功能,修改方式为:选中 Target
为刚刚创建的 Extension,在 Info 中将 NSExtension > NSExtensionPrincipalClass 所对应的 Value 从 SampleHandler 改为 AgoraReplayKitHandler。
如果你还需要自定义一些业务逻辑,修改方式为:将如下代码替换到 SampleHandler.h
文件中:
// Objective-C
#import "SampleHandler.h"
#import "AgoraReplayKitExt.h"
#import <sys/time.h>
@interface SampleHandler ()<AgoraReplayKitExtDelegate>
@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.
[[AgoraReplayKitExt shareInstance] start:self];
}
- (void)broadcastPaused {
// User has requested to pause the broadcast. Samples will stop being delivered.
NSLog(@"broadcastPaused");
[[AgoraReplayKitExt shareInstance] pause];
}
- (void)broadcastResumed {
// User has requested to resume the broadcast. Samples delivery will resume.
NSLog(@"broadcastResumed");
[[AgoraReplayKitExt shareInstance] resume];
}
- (void)broadcastFinished {
// User has requested to finish the broadcast.
NSLog(@"broadcastFinished");
[[AgoraReplayKitExt shareInstance] stop];
}
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer withType:(RPSampleBufferType)sampleBufferType {
[[AgoraReplayKitExt shareInstance] pushSampleBuffer:sampleBuffer withType:sampleBufferType];
}
#pragma mark - AgoraReplayKitExtDelegate
- (void)broadcastFinished:(AgoraReplayKitExt *_Nonnull)broadcast reason:(AgoraReplayKitExtReason)reason {
switch (reason) {
case AgoraReplayKitExtReasonInitiativeStop:
{
// NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"Host app stop srceen capture"};
// NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo];
// [self finishBroadcastWithError:error];
NSLog(@"AgoraReplayKitExtReasonInitiativeStop");
}
break;
case AgoraReplayKitExtReasonConnectFail:
{
// NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"Connect host app fail need startScreenCapture in host app"};
// NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo];
// [self finishBroadcastWithError:error];
NSLog(@"AgoraReplayKitExReasonConnectFail");
}
break;
case AgoraReplayKitExtReasonDisconnect:
{
// NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"disconnect with host app"};
// NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:userInfo];
// [self finishBroadcastWithError:error];
NSLog(@"AgoraReplayKitExReasonDisconnect");
}
break;
default:
break;
}
}
@end
调用 startScreenCapture
,并结合用户的手动操作,使 app 开启屏幕共享。有两种方式供你参考:
方式一:提示用户在 iOS 系统的控制中心长按屏幕录制按钮,并选择用你创建的 Extension 开启录制。
方式二:使用 Apple 在 iOS 12.0 中新增的 RPSystemBroadcastPickerView,使 app 界面弹出“开启屏幕共享”的按钮。提示用户通过点击该按钮开启录制。
RPSystemBroadcastPickerView
存在一些使用限制并可能在之后版本的 iOS 系统中失效。因此,请酌情使用方式二。声网在 GitHub 提供一个开源示例项目,你可以参考其中的如下文件:
/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift
/Agora-ScreenShare-Extension/SampleHandler.swift
屏幕共享功能目前存在一些使用限制和注意事项,同时会产生费用,声网推荐你在调用 API 前先阅读如下 API 参考:
startScreenCapture
updateScreenCapture
stopScreenCapture
本节介绍如何使用早于 3.7.0 版的 iOS SDK 实现屏幕共享。
在实现屏幕共享前,请确保已在你的项目中实现基本的实时音视频功能。详见开始音视频通话或开始互动直播。
参考以下步骤,创建一个 Broadcast Upload Extension。
用 Xcode 打开项目的工程文件,在菜单栏中选择 Editor > Add Target...。
在弹出窗口中,选择 iOS 页的 Broadcast Upload Extension,点击 Next。
在 Product Name 一栏输入 Extension 的名字,如 Agora-ScreenShare,然后点击 Finish。
创建完成后,你会在项目中看到该 Extension 的文件夹,用于存放屏幕共享功能的实现代码。
打开项目中的 Podfile,为 Extension 添加依赖项:
target 'Agora-ScreenShare-Extension' do
use_frameworks!
pod 'AgoraRtcEngine_iOS', '~> 3.1.1'
# 如果需要媒体流加密功能,使用下面的代码
# pod 'AgoraRtcEngine_iOS_Crypto', '~> 3.1.1'
end
在项目根目录下运行 pod install
命令,安装依赖项。
将 RPSystemBroadcastPickerView
作为启动屏幕录制的按钮添加到 app 的 view 中。
// Swift
func prepareSystemBroadcaster() {
if #available(iOS 12.0, *) {
let frame = CGRect(x: 0, y:0, width: 60, height: 60)
let systemBroadcastPicker = RPSystemBroadcastPickerView(frame: frame)
systemBroadcastPicker.autoresizingMask = [.flexibleTopMargin, .flexibleRightMargin]
if let url = Bundle.main.url(forResource: "Agora-ScreenShare-Extension", withExtension: "appex", subdirectory: "PlugIns") {
if let bundle = Bundle(url: url) {
systemBroadcastPicker.preferredExtension = bundle.bundleIdentifier
}
}
broadcasterPickerContainer.addSubview(systemBroadcastPicker)
} else {
self.showAlert(message: "Minimum support iOS version is 12.0")
}
}
通过 SDK 自定义视频采集的功能,实现将系统录制的屏幕数据发送给远端用户,进行屏幕共享。
为屏幕共享流创建一个 AgoraRtcEngineKit
实例,并设置自定义视频采集。
// Swift
private static let sharedAgoraEngine: AgoraRtcEngineKit = {
let kit = AgoraRtcEngineKit.sharedEngine(withAppId: KeyCenter.AppId, delegate: nil)
kit.enableVideo()
// 使用自定义视频源
kit.setExternalVideoSource(true, useTexture: true, pushMode: true)
// 不接收所有远端用户的视频流和音频流
kit.muteAllRemoteVideoStreams(true)
kit.muteAllRemoteAudioStreams(true)
return kit
}()
使用 Apple 原生的 ReplayKit 框架实现屏幕录制。
在屏幕共享 Extension 的 Info.plist
文件中,将 RPBroadcastProcessMode
设置为 RPBroadcastProcessModeSampleBuffer
。
在 broadcastStarted
回调中开始录制屏幕。
ScreenShare
;如果你需要让用户输入频道名,必须使用 App Group 将主进程的参数值传递给 Extension。// Swift
override func broadcastStarted(withSetupInfo setupInfo: [String : NSObject]?) {
AgoraUploader.startBroadcast(to: "ScreenShare")
DispatchQueue.main.async {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {[weak self] (timer:Timer) in
guard let weakSelf = self else {return}
let elapse = Int64(Date().timeIntervalSince1970 * 1000) - weakSelf.lastSendTs
print("elapse: \(elapse)")
// 视频帧间隔过长时,重新发送上一帧。
if(elapse > 300) {
if let buffer = weakSelf.bufferCopy {
weakSelf.processSampleBuffer(buffer, with: .video)
}
}
}
}
}
系统采集到数据后发送给 SDK。
// Swift
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) {
DispatchQueue.main.async {[weak self] in
switch sampleBufferType {
case .video:
if let weakSelf = self {
weakSelf.bufferCopy = sampleBuffer
weakSelf.lastSendTs = Int64(Date().timeIntervalSince1970 * 1000)
}
// 发送视频 buffer
AgoraUploader.sendVideoBuffer(sampleBuffer)
@unknown default:
break
}
}
}
通过 pushExternalVideoFrame
实现 sendVideoBuffer
。
// Swift
static func sendVideoBuffer(_ sampleBuffer: CMSampleBuffer) {
guard let videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer)
else {
return
}
var rotation : Int32 = 0
if let orientationAttachment = CMGetAttachment(sampleBuffer, key: RPVideoSampleOrientationKey as CFString, attachmentModeOut: nil) as? NSNumber {
if let orientation = CGImagePropertyOrientation(rawValue: orientationAttachment.uint32Value) {
switch orientation {
case .up, .upMirrored: rotation = 0
case .down, .downMirrored: rotation = 180
case .left, .leftMirrored: rotation = 90
case .right, .rightMirrored: rotation = 270
default: break
}
}
}
let time = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: 1000)
let frame = AgoraVideoFrame()
frame.format = 12
frame.time = time
frame.textureBuf = videoFrame
frame.rotation = rotation
sharedAgoraEngine.pushExternalVideoFrame(frame)
}
在屏幕共享流对应的 AgoraRtcEngineKit
实例中调用 joinChannelByToken
加入频道,就可以开始屏幕共享。
// Swift
static func startBroadcast(to channel: String) {
print("joining \(channel)")
sharedAgoraEngine.joinChannel(byToken: KeyCenter.Token, channelId: channel, info: nil, uid: SCREEN_SHARE_UID, joinSuccess: nil)
}
使用早于 3.7.0 版的 iOS SDK 实现屏幕共享的方法较之复杂,为降低你使用示例项目的门槛,本节对示例项目使用的文件进行说明:
文件/文件夹 | 描述 |
---|---|
/iOS/Agora-ScreenShare-Extension |
用于屏幕共享进程的 Extension,主要的代码文件如下:
|
/iOS/APIExample/Examples/Advanced/ScreenShare/ScreenShare.swift |
屏幕共享 app 的主要代码,实现本地用户加入频道和开启屏幕共享的功能。 |
/iOS/Agora-ScreenShare-Extension/AgoraAudioTube.mm |
通过自定义音频采集实现,在屏幕共享时需要同时分享 app 的音频。 |
如果你使用 Cocoapods,则需要在 Podfile
文件中添加如下内容,为你的屏幕共享 Extension 添加依赖。注意将 ScreenSharing
替换为你的屏幕共享 Extension 的 Target 名称。
target 'ScreenSharing' do
pod 'AgoraRtcEngine_iOS', '3.7.0'
end
Broadcast Upload Extension 的内存使用限制为 50 MB,请确保屏幕共享的 Extension 内存使用不超过 50 MB。
屏幕共享的进程中,需要调用 muteAllRemoteVideoStreams
和 muteAllRemoteAudioStreams
方法取消接收远端用户的流,避免重复订阅。
对于主进程来说,屏幕共享流在频道中相当于一个远端用户。为避免不必要的费用,在设置远端用户视图时可以将屏幕共享流除外。
// Swift
// 判断是否为屏幕共享流的 uid
func isScreenShareUid(uid: UInt) -> Bool {
return uid >= SCREEN_SHARE_UID_MIN && uid <= SCREEN_SHARE_UID_MAX
}
// 远端用户加入频道回调
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
LogUtils.log(message: "remote user join: \(uid) \(elapsed)ms", level: .info)
// 如果是屏幕共享的 uid,忽略
if(isScreenShareUid(uid: uid)) {
LogUtils.log(message: "Ignore screen share uid", level: .info)
return
}
// 设置远端用户视图
......
}