摘要:
在在线教育、远程协助、App 操作演示、移动端业务培训等场景中,开发者经常需要把 iPhone 或 iPad 的系统屏幕内容实时推送到 RTMP 服务器,实现"同屏直播"或"屏幕共享"。本文基于 SmartiOSScreenPublisherV2 示例工程,介绍如何通过 ReplayKit Broadcast Upload Extension 采集 iOS 系统屏幕、App 音频和麦克风音频,并结合大牛直播 SDK(SmartMediaKit)实现低延迟 RTMP 同屏推流。
关键词:大牛直播SDK、SmartMediaKit、iOS同屏直播、iOS屏幕采集、ReplayKit、Broadcast Upload Extension、RTMP推流、屏幕共享、低延迟直播
一、背景
在在线教育、远程协助、App 操作演示、移动端业务培训等场景中,开发者经常需要把 iPhone 或 iPad 的屏幕内容实时推送到 RTMP 服务器,实现"同屏直播"或"屏幕共享"。
和普通摄像头推流不同,iOS 系统屏幕采集不能简单地在主 App 中长期直接拿到完整屏幕帧,而是需要通过 Apple 提供的 ReplayKit Broadcast Upload Extension 机制完成。
大牛直播 SDK(SmartMediaKit)iOS 推流模块支持接收 ReplayKit 回调的 CMSampleBufferRef,并通过 H.264 硬编码、AAC 编码实现低延迟 RTMP 同屏直播。
本文基于 SmartiOSScreenPublisherV2 示例工程,介绍 iOS 平台如何通过系统屏幕采集实现同屏 RTMP 推流。
二、功能特性
SmartiOSScreenPublisherV2 示例工程主要能力如下:
| 功能 | 说明 |
|---|---|
| 系统屏幕采集 | 基于 ReplayKit Broadcast Upload Extension |
| 屏幕视频推流 | 接收 RPSampleBufferTypeVideo 并投递给 SmartPublisherSDK |
| App 音频采集 | 支持 RPSampleBufferTypeAudioApp |
| 麦克风采集 | 支持 RPSampleBufferTypeAudioMic |
| 音频源切换 | 主 App 可在 App 音频和麦克风之间切换 |
| RTMP 推流 | 扩展进程内启动 SmartPublisherSDK 推流 |
| H.264 硬编码 | 使用 VideoToolbox 硬编码 |
| AAC 音频编码 | 支持外部音频 CMSampleBufferRef 投递 |
| 后台屏幕推流模式 | SDK 设置 SmartPublisherSetSDKRunMode:1 |
| 事件回调 | 支持连接中、已连接、断线、发送堆积等事件日志 |
| 内存保护 | Broadcast Extension 内存受限,可按阈值主动丢帧 |
三、工程结构

其中最核心的是:
DaniuliveExt/SampleHandler.m
ReplayKit 会把屏幕视频、App 音频、麦克风音频回调到 SampleHandler,然后由 SampleHandler 投递给 SmartPublisherSDK 进行编码和 RTMP 推流。
四、为什么需要 Broadcast Extension?
iOS 系统屏幕采集由 ReplayKit 管理。典型同屏直播不是主 App 自己直接采集屏幕,而是通过 Broadcast Upload Extension 完成。

这样设计的原因主要有三点:
- iOS 对系统屏幕采集权限控制严格,必须由用户主动触发系统广播;
- Broadcast Extension 可以在主 App 切后台或切到其他 App 时继续接收屏幕帧;
- 用户通过系统录屏入口明确授权,符合 iOS 隐私模型。
因此,iOS 同屏直播的真正推流逻辑,一般不在主 App 的 ViewController 中,而是在 Broadcast Upload Extension 的 SampleHandler 中。
五、工程集成
1. 添加 SDK
将编译好的:
objectivec
SmartPublisherSDKAll.xcframework
放到扩展 target 的 SDK 目录,例如:
objectivec
SmartiOSScreenPublisherV2/DaniuliveExt/daniuliveSDK/libs/
同时确保扩展 target 能访问:
objectivec
DaniuliveExt/daniuliveSDK/include/SmartPublisherSDK.h
DaniuliveExt/daniuliveSDK/include/nt_event_define.h
注意:屏幕推流真正发生在 Broadcast Upload Extension 进程内,所以 SDK 必须链接到 DaniuliveExt target,而不仅仅是主 App target。
2. 链接系统库
Broadcast Upload Extension target 需要链接:
objectivec
ReplayKit.framework
AVFoundation.framework
VideoToolbox.framework
AudioToolbox.framework
CoreMedia.framework
CoreVideo.framework
Foundation.framework
UIKit.framework
libz.tbd
libbz2.tbd
libiconv.tbd
libc++.tbd
根据 SDK 打包方式,工程中还可能需要链接:
objectivec
CFNetwork.framework
Security.framework
如果使用 SmartPublisherSDKAll.xcframework,建议重点检查该 xcframework 是否已经正确加入 DaniuliveExt target 的:
objectivec
Target -> General -> Frameworks, Libraries, and Embedded Content
静态 xcframework 通常选择:
Do Not Embed
3. Broadcast Extension 配置
DaniuliveExt/Info.plist 中需要配置:
objectivec
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.broadcast-services-upload</string>
<key>NSExtensionPrincipalClass</key>
<string>SampleHandler</string>
<key>RPBroadcastProcessMode</key>
<string>RPBroadcastProcessModeSampleBuffer</string>
</dict>
麦克风采集需要:
objectivec
<key>NSMicrophoneUsageDescription</key>
<string>请允许使用麦克风</string>
主 App 如果需要提示麦克风权限,也可以在主 App 的 Info.plist 中增加对应权限描述。
六、App Group 配置
主 App 和 Broadcast Extension 是两个不同进程。主 App 中输入的 RTMP 地址、音频源选择,需要通过 App Group 共享给扩展。

示例工程使用:
objectivec
static NSString * const kAppGroupID = @"group.com.daniulive.screenpublisher";
static NSString * const kPublishURLKey = @"rtmp_publish_url";
static NSString * const kAudioSourceKey = @"audio_source_prefer_mic";
需要在 Xcode 的两个 target 中同时添加同一个 App Group:
objectivec
Signing & Capabilities
-> + Capability
-> App Groups
-> group.com.daniulive.screenpublisher
主 App 保存 RTMP 地址:
objectivec
NSUserDefaults *shared =
[[NSUserDefaults alloc] initWithSuiteName:kAppGroupID];
[shared setObject:url forKey:kPublishURLKey];
[shared synchronize];
扩展启动时读取:
objectivec
NSUserDefaults *shared =
[[NSUserDefaults alloc] initWithSuiteName:kAppGroupID];
NSString *url = [shared stringForKey:kPublishURLKey];
_publisherURL = (url.length > 0) ? url : kDefaultPublishURL;
这里需要特别注意:ReplayKit 启动 Broadcast Extension 后,SampleHandler 进程无法直接访问主 App 内存中的变量。因此推流地址、音频源偏好等配置,必须在启动广播前写入 App Group。
七、主 App 启动系统同屏
主 App 使用 RPSystemBroadcastPickerView 唤起系统广播选择器。
创建广播选择器:
objectivec
self.broadcastPickerView =
[[RPSystemBroadcastPickerView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
self.broadcastPickerView.showsMicrophoneButton = YES;
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];
self.broadcastPickerView.preferredExtension =
[bundleId stringByAppendingString:@".DaniuliveExt"];
[self.view addSubview:self.broadcastPickerView];
点击"开始直播"时,示例工程先保存推流地址,然后触发系统广播按钮:
objectivec
NSUserDefaults *shared =
[[NSUserDefaults alloc] initWithSuiteName:kAppGroupID];
[shared setObject:url forKey:kPublishURLKey];
[shared synchronize];
for (UIView *view in self.broadcastPickerView.subviews) {
if ([view isKindOfClass:[UIButton class]]) {
[(UIButton *)view sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
用户确认后,系统会启动 DaniuliveExt 扩展。后续屏幕采集和推流逻辑都在扩展进程中执行。
从主 App 的角度看,它主要负责:
- 输入 RTMP 推流地址;
- 选择音频源;
- 写入 App Group;
- 唤起 ReplayKit 系统广播面板;
- 提示用户如何停止系统广播。
八、扩展端初始化 SDK

SampleHandler 是 ReplayKit 的核心入口:
objectivec
@interface SampleHandler : RPBroadcastSampleHandler
@end
广播开始时,系统会调用:
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo
示例逻辑如下:
objectivec
- (void)broadcastStartedWithSetupInfo:(NSDictionary<NSString *,NSObject *> *)setupInfo {
NSUserDefaults *shared =
[[NSUserDefaults alloc] initWithSuiteName:kAppGroupID];
NSString *url = [shared stringForKey:kPublishURLKey];
_publisherURL = (url.length > 0) ? url : kDefaultPublishURL;
_preferMic = [self readAudioSourcePreference];
if ([self initPublisher]) {
[self startPublisher];
}
}
初始化 SmartPublisherSDK:
objectivec
- (BOOL)initPublisher {
_sdk = [[SmartPublisherSDK alloc] init];
_sdk.delegate = self;
// audio_opt = 3: 外部 CMSampleBuffer 音频
// video_opt = 3: 外部 CMSampleBuffer 视频
NSInteger ret = [_sdk SmartPublisherInit:3 video_opt:3];
if (ret != DANIULIVE_RETURN_OK) {
NSLog(@"SmartPublisherInit failed");
_sdk = nil;
return NO;
}
[_sdk SmartPublisherSetFPS:25];
[_sdk SmartPublisherSetVideoBitRate:2500 maxBitRate:5000];
[_sdk SmartPublisherSetGopInterval:75];
[_sdk SmartPublisherSetVideoSizeScaleRate:0.8f];
// H.264 硬编码
[_sdk SmartPublisherSetVideoEncoderType:1 isHwEncoder:YES];
// AAC 音频编码
[_sdk SmartPublisherSetAudioEncoderType:1 isHwEncoder:NO];
// 后台屏幕推流模式
[_sdk SmartPublisherSetSDKRunMode:1];
return YES;
}
这里使用:
[_sdk SmartPublisherInit:3 video_opt:3];
原因是屏幕视频、App 音频、麦克风音频都来自 ReplayKit 回调,属于外部 CMSampleBufferRef 数据投递。
SmartPublisherSetSDKRunMode:1 是屏幕推流场景中的关键设置,用于适配 Broadcast Extension 场景下的运行模式。
启动 RTMP 推流:
objectivec
- (BOOL)startPublisher {
if (_sdk == nil || _publisherURL.length == 0) {
return NO;
}
NSInteger ret = [_sdk SmartPublisherStartPublisher:_publisherURL];
if (ret != DANIULIVE_RETURN_OK) {
NSLog(@"SmartPublisherStartPublisher failed");
return NO;
}
return YES;
}
九、处理 ReplayKit 音视频帧

ReplayKit 会通过以下方法持续回调屏幕和音频数据:
objectivec
- (void)processSampleBuffer:(CMSampleBufferRef)sampleBuffer
withType:(RPSampleBufferType)sampleBufferType
1. 屏幕视频帧
objectivec
- (void)handleVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
if (!CMSampleBufferIsValid(sampleBuffer) || _sdk == nil) {
return;
}
NSInteger rotateDegrees = [self rotateDegreesFromSampleBuffer:sampleBuffer];
[_sdk SmartPublisherPostVideoSampleBufferV2:sampleBuffer
rotateDegress:rotateDegrees];
}
如果系统版本支持 RPVideoSampleOrientationKey,可以读取屏幕方向并转换为旋转角度,方向映射关系可以理解为:
Up -> 0
Down -> 180
Left -> 90
Right -> 270
2. App 音频
objectivec
- (void)handleAppAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer {
if (_preferMic) {
return;
}
if (_sdk != nil && CMSampleBufferDataIsReady(sampleBuffer)) {
[_sdk SmartPublisherPostAudioSampleBuffer:sampleBuffer inputType:1];
}
}
3. 麦克风音频
objectivec
- (void)handleMicAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer {
if (!_preferMic) {
return;
}
if (_sdk != nil && CMSampleBufferDataIsReady(sampleBuffer)) {
[_sdk SmartPublisherPostAudioSampleBuffer:sampleBuffer inputType:1];
}
}
这里 inputType:1 表示主音频流。示例工程选择在上层做音频源切换,而不是同时向 SDK 投递两路音频,这样可以避免多路音频时间线导致的混音、延迟或噪声问题。
十、音频源动态切换
SmartiOSScreenPublisherV2 支持推流中切换音频源:
- App 音频,也就是系统声音;
- 麦克风音频。
主 App 写入偏好:
objectivec
NSUserDefaults *shared =
[[NSUserDefaults alloc] initWithSuiteName:kAppGroupID];
[shared setBool:preferMic forKey:kAudioSourceKey];
[shared synchronize];
然后通过 Darwin Notification 通知扩展进程刷新:
objectivec
CFNotificationCenterPostNotification(
CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge CFStringRef)kAudioSourceNotif,
NULL,
NULL,
YES);
扩展端监听通知:
objectivec
CFNotificationCenterAddObserver(
CFNotificationCenterGetDarwinNotifyCenter(),
(__bridge const void *)self,
AudioSourceChangedCallback,
(__bridge CFStringRef)kAudioSourceNotif,
NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
Darwin Notification 回调:
objectivec
static void AudioSourceChangedCallback(CFNotificationCenterRef center,
void *observer,
CFStringRef name,
const void *object,
CFDictionaryRef userInfo) {
SampleHandler *handler = (__bridge SampleHandler *)observer;
[handler refreshAudioSourcePreference];
}
扩展端重新读取 App Group:
- (void)refreshAudioSourcePreference {
_preferMic = [self readAudioSourcePreference];
}
读取音频源偏好:
objectivec
- (BOOL)readAudioSourcePreference {
NSUserDefaults *shared =
[[NSUserDefaults alloc] initWithSuiteName:kAppGroupID];
return [shared boolForKey:kAudioSourceKey];
}
需要注意:选择麦克风时,用户还需要在 ReplayKit 开播弹窗里打开麦克风开关,否则 RPSampleBufferTypeAudioMic 不会持续回调。
十一、结束同屏直播

ReplayKit 广播结束时,系统会调用:
- (void)broadcastFinished
示例工程中按顺序停止推流并释放 SDK:
objectivec
- (void)broadcastFinished {
[self stopPublisher];
[self uninitPublisher];
}
停止推流:
- (void)stopPublisher {
if (_sdk != nil) {
[_sdk SmartPublisherStopPublisher];
}
}
释放 SDK:
objectivec
- (void)uninitPublisher {
if (_sdk != nil) {
_sdk.delegate = nil;
[_sdk SmartPublisherUnInit];
_sdk = nil;
}
}
用户侧停止方式通常有三种:
- 点击 iOS 顶部红色录制指示器;
- 下拉控制中心,点击录屏按钮停止;
- 系统因异常调用
finishBroadcastWithError:。
异常结束时可以调用:
objectivec
NSError *error =
[NSError errorWithDomain:@"com.daniulive.screenpublisher"
code:-1
userInfo:@{
NSLocalizedDescriptionKey: @"推流异常结束"
}];
[self finishBroadcastWithError:error];
十二、事件回调
扩展端实现 SmartPublisherDelegate:
objectivec
- (NSInteger)handleSmartPublisherEvent:(NSInteger)nID
param1:(unsigned long long)param1
param2:(unsigned long long)param2
param3:(NSString *)param3
param4:(NSString *)param4
pObj:(void *)pObj
{
switch (nID) {
case EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING:
NSLog(@"连接中");
break;
case EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED:
NSLog(@"已连接");
break;
case EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED:
NSLog(@"连接失败,SDK 将自动重连");
break;
case EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED:
NSLog(@"连接断开,SDK 将自动重连");
break;
case EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY:
NSLog(@"发送堆积 %llums 队列包数=%llu", param1, param2);
break;
case EVENT_DANIULIVE_ERC_PUBLISHER_STOP:
NSLog(@"停止推流");
break;
default:
NSLog(@"event id=0x%lx", (long)nID);
break;
}
return 0;
}
Broadcast Extension 没有普通 App 那样的完整 UI,所以事件一般写日志,或通过 App Group、Darwin Notification、文件日志等机制同步给主 App。
常见事件说明如下:
|---------------------------------------------------|----------------------|
| 事件 | 说明 |
| EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTING | RTMP 连接中 |
| EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTED | RTMP 已连接 |
| EVENT_DANIULIVE_ERC_PUBLISHER_CONNECTION_FAILED | RTMP 连接失败,SDK 将尝试重连 |
| EVENT_DANIULIVE_ERC_PUBLISHER_DISCONNECTED | RTMP 断开,SDK 将尝试重连 |
| EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY | 发送堆积,说明网络或服务端处理可能有压力 |
| EVENT_DANIULIVE_ERC_PUBLISHER_STOP | 推流停止 |
十三、内存控制

Broadcast Extension 的内存限制比主 App 更严格。示例工程中增加了简单的内存保护:
当扩展进程内存占用超过阈值时,直接丢弃当前帧,避免系统杀掉扩展:
objectivec
- (BOOL)shouldDropFrameForMemoryPressure {
CGFloat usedMB = (CGFloat)GetCurUsedMemory() / 1024.0f / 1024.0f;
if (usedMB > kMemoryDropThresholdMB) {
return YES;
}
return NO;
}
获取当前内存占用可以参考:
objectivec
static int64_t GetCurUsedMemory(void) {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t kernReturn =
task_info(mach_task_self(),
TASK_VM_INFO,
(task_info_t)&vmInfo,
&count);
if (kernReturn != KERN_SUCCESS) {
return 0;
}
return (int64_t)vmInfo.phys_footprint;
}
实际产品中建议:
- 控制输出分辨率和帧率;
- 合理设置
SmartPublisherSetVideoSizeScaleRate; - 避免在
processSampleBuffer中做重 CPU 操作; - 不要长期持有
CMSampleBufferRef; - 日志不要过于频繁;
- 避免在扩展中做复杂滤镜、逐像素转换和大对象缓存。
十四、低延迟建议
iOS 同屏推流要做到低延迟,建议:
- 使用 ReplayKit 原始
CMSampleBufferRef直接投递给 SDK; - 使用 VideoToolbox H.264 硬编码;
- GOP 设置为 2 到 3 秒以内;
- 根据屏幕分辨率合理设置码率;
- 不做额外滤镜、水印、复杂图层合成;
- 关注
EVENT_DANIULIVE_ERC_PUBLISHER_SEND_DELAY事件,判断网络发送是否堆积; - Broadcast Extension 中不要做过多日志和耗时操作;
- 对高分辨率屏幕采集场景,建议适当降低输出缩放比例。
示例配置:
objectivec
[_sdk SmartPublisherSetFPS:25];
[_sdk SmartPublisherSetVideoBitRate:2500 maxBitRate:5000];
[_sdk SmartPublisherSetGopInterval:75];
[_sdk SmartPublisherSetVideoSizeScaleRate:0.8f];
这里 VideoSizeScaleRate 设置为 0.8f,主要是为了降低屏幕采集分辨率带来的编码和网络压力。
十五、常见问题
1. 为什么主 App 里看不到屏幕帧?
因为 iOS 系统屏幕采集帧回调在 Broadcast Upload Extension 的 SampleHandler 中,不在主 App 的 ViewController 中。
主 App 只是负责配置参数、写入 App Group 和唤起系统广播。
2. 为什么选择麦克风后没有声音?
需要两个条件同时满足:
objectivec
主 App 音频源选择为"麦克风"
用户在 ReplayKit 开播弹窗中打开麦克风开关
否则 RPSampleBufferTypeAudioMic 不会持续回调。
3. 为什么要用 App Group?
主 App 和 Broadcast Extension 是两个不同进程。RTMP 地址、音频源偏好等配置需要通过 App Group 共享。
4. 为什么推流地址要先保存?
ReplayKit 启动扩展后,SampleHandler 进程无法直接访问主 App 的内存变量,只能从 App Group 中读取启动前写入的推流地址。
5. 为什么不建议在扩展中做复杂处理?
Broadcast Extension 内存和 CPU 资源受限。复杂滤镜、逐像素转换、频繁日志、长时间持有 CMSampleBufferRef,都可能导致扩展被系统终止。
6. 为什么同屏直播只能用户主动开始?
这是 iOS 的系统安全机制。屏幕采集需要用户通过系统广播面板授权,App 不能绕过系统机制静默开启系统级屏幕采集。
十六、典型应用场景
1. App 操作同屏直播
适合在线教学、产品演示、业务培训等场景。用户可以实时展示 App 操作流程,远端通过 RTMP 观看。
2. 远程协助
用户开启同屏后,技术支持人员可以实时查看用户操作流程,适合远程排障、客服指导、移动端业务培训等场景。
3. 移动端业务录制与审计
适合金融、保险、政企类 App 的操作过程留痕和实时监管,也可以配合业务端录像服务完成操作审计。
十七、集成注意事项
1. SDK 必须链接到 Broadcast Upload Extension target
同屏推流发生在 DaniuliveExt 进程内,所以 SDK 必须加入扩展 target。
只加入主 App target 是不够的。
2. 主 App 和扩展必须配置同一个 App Group
RTMP 地址、音频源偏好等配置都依赖 App Group 共享。
3. 推流地址应在启动广播前写入 App Group
否则扩展启动后可能读不到正确的推流地址,只能使用默认地址。
4. 麦克风开关需要用户在系统开播弹窗中确认
即使主 App 选择了"麦克风",如果用户在 ReplayKit 开播弹窗中没有打开麦克风,扩展也不会收到麦克风数据。
5. processSampleBuffer 中不要异步长期持有 CMSampleBufferRef
ReplayKit 回调频率高,扩展内存受限。建议直接同步投递给 SDK。
6. 扩展进程内存限制较严格
建议控制分辨率、码率、缩放比例和日志量。
7. 停止直播由系统广播机制触发
主 App 不能像普通推流一样完全控制系统广播停止,只能通过 UI 提示用户点击顶部红色录制指示器或控制中心录屏按钮停止。
8. 若扩展中增加 RTSP 或录像能力,需要谨慎评估
Broadcast Extension 生命周期、内存和 CPU 都受限制。如果需要 RTSP、录像等能力,建议结合具体业务谨慎设计,不建议一开始把扩展做得过重。
十八、总结
大牛直播 SDK(SmartMediaKit)iOS 同屏直播方案,基于 ReplayKit Broadcast Upload Extension 获取系统屏幕、App 音频和麦克风数据,并通过 SmartPublisherSDK 实现 H.264 硬编码、AAC 编码和 RTMP 低延迟推流。
SmartiOSScreenPublisherV2 示例工程清晰地拆分了主 App 和 Broadcast Extension 的职责:主 App 负责配置推流地址、选择音频源和唤起系统广播;扩展负责接收 ReplayKit 的音视频帧、初始化 SDK、启动 RTMP 推流并处理事件回调。
对于需要在 iPhone 或 iPad 上实现系统级屏幕共享、App 操作同屏、远程协助、移动端业务培训和低延迟屏幕推流的开发者来说,该示例可以作为一个轻量、清晰、易集成的参考实现。
📎 CSDN官方博客:音视频牛哥-CSDN博客