如何将RPBroadcastSampleHandler中的音频数据发送至Flutter主应用进程的插件?
如何将RPBroadcastSampleHandler中的音频数据发送至Flutter主应用进程的插件?
嘿,我完全懂你现在的头疼——刚摸iOS开发,还要搞Flutter插件和Broadcast Extension的跨进程通信,CFMessagePort确实对新手不太友好,我给你几个上手快、靠谱的方案,一步步来:
先搞定基础配置:App Groups
不管用哪种跨进程通信方法,这一步是必须的,因为iOS要求跨进程共享资源必须有App Groups权限:
- 打开你的Flutter项目的iOS工程(直接用Xcode打开ios文件夹下的
Runner.xcworkspace就行) - 分别选中左侧导航栏里的主App Target(一般叫Runner)和你的Broadcast Upload Extension Target
- 切换到「Signing & Capabilities」标签页,点击左上角的「+ Capability」,搜索并添加「App Groups」
- 给两个Target都添加同一个App Group标识,格式必须是
group.开头,比如group.com.yourteam.yourapp,确保两个Target的这个标识完全一致
方案一:Darwin通知中心 + 共享文件容器(最适合音频这类大数据)
这个方案逻辑很简单:Extension把音频数据存到共享文件夹,然后发个通知告诉主App“有新数据啦”,主App收到通知后去读文件,再传给Flutter Dart层,新手好理解也好实现。
1. 在Broadcast Extension的RPBroadcastSampleHandler里处理并存储音频数据
先写个处理音频样本的辅助方法,把CMSampleBuffer转成Data,存到共享容器:
import ReplayKit import Foundation class SampleHandler: RPBroadcastSampleHandler { let appGroupID = "group.com.yourteam.yourapp" private func saveAudioSample(_ sampleBuffer: CMSampleBuffer) { // 先获取共享容器的路径 guard let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID) else { print("获取共享容器失败,检查App Groups配置!") return } // 把CMSampleBuffer转成Data guard let dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer) else { return } var audioData = Data() let dataLength = CMBlockBufferGetDataLength(dataBuffer) var bufferPointer: UnsafeMutablePointer<Int8>? let status = CMBlockBufferAccessDataBytes(dataBuffer, atOffset: 0, length: dataLength, bufferListOut: nil, bufferPointerOut: &bufferPointer) if status == noErr, let pointer = bufferPointer { audioData.append(pointer, count: dataLength) } // 生成唯一的文件名,防止覆盖之前的数据 let fileName = "audio_chunk_\(UUID().uuidString).dat" let fileURL = sharedContainerURL.appendingPathComponent(fileName) // 写入共享文件夹 do { try audioData.write(to: fileURL) // 发通知告诉主App来读数据 let notificationName = "com.yourteam.audio_data_available" as CFString CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), notificationName, nil, nil, true) } catch { print("存储音频数据失败: \(error.localizedDescription)") } } // 系统回调,收到音频/视频样本时触发 override func broadcastSampleHandler(_ sampleBuffer: CMSampleBuffer, for sampleType: RPSampleBufferType) { switch sampleType { case .audioApp: // 捕获到其他App的音频 saveAudioSample(sampleBuffer) default: // 视频或者其他类型的样本,按需处理 break } } }
2. 在主App的iOS端监听通知并读取数据,传给Flutter
主App的iOS部分(可以在你的Flutter插件类里,或者AppDelegate中)注册Darwin通知监听,收到通知后读取共享文件,再通过MethodChannel传给Flutter:
import Flutter import Foundation // 假设这是你的Flutter插件类 class YourBroadcastPlugin: NSObject, FlutterPlugin { static let appGroupID = "group.com.yourteam.yourapp" static let channelName = "com.yourteam/broadcast_audio" var channel: FlutterMethodChannel? static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel(name: channelName, binaryMessenger: registrar.messenger()) let instance = YourBroadcastPlugin() instance.channel = channel registrar.addMethodCallDelegate(instance, channel: channel) // 在这里注册Darwin通知监听 let notificationName = "com.yourteam.audio_data_available" as CFString CFNotificationCenterAddObserver( CFNotificationCenterGetDarwinNotifyCenter(), nil, { (_, _, _, _, _) in instance.readSharedAudioFiles() }, notificationName, nil, .deliverImmediately ) } private func readSharedAudioFiles() { guard let sharedContainerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Self.appGroupID) else { print("无法访问共享容器,请检查App Groups配置") return } do { // 遍历共享文件夹里的音频文件 let files = try FileManager.default.contentsOfDirectory(at: sharedContainerURL, includingPropertiesForKeys: nil) for fileURL in files where fileURL.pathExtension == "dat" { // 读取文件数据 let audioData = try Data(contentsOf: fileURL) // 转成Base64字符串传给Flutter(因为MethodChannel传二进制数据用Base64更方便) let base64Data = audioData.base64EncodedString() channel?.invokeMethod("receive_audio_data", arguments: base64Data) // 读完立刻删除文件,不然共享容器会越变越大 try FileManager.default.removeItem(at: fileURL) } } catch { print("读取共享文件失败: \(error.localizedDescription)") } } // 处理Flutter端的方法调用(按需实现) func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { switch call.method { // 比如Flutter端请求开启广播之类的方法,按需加 default: result(FlutterMethodNotImplemented) } } }
3. 在Flutter Dart层接收并处理音频数据
在你的Flutter页面或者插件的Dart部分,初始化MethodChannel,监听主App传过来的音频数据:
import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/services.dart'; class BroadcastAudioReceiver { static const MethodChannel _channel = MethodChannel('com.yourteam/broadcast_audio'); void initReceiver() { _channel.setMethodCallHandler((call) async { if (call.method == 'receive_audio_data') { // 把Base64字符串转回二进制数据 String base64Str = call.arguments; Uint8List audioData = base64Decode(base64Str); // 这里就可以处理音频数据了,比如播放、存储或者做其他处理 _processAudioData(audioData); } }); } void _processAudioData(Uint8List data) { // 你的音频处理逻辑,比如转成音频流播放 print("收到音频数据,长度: ${data.length}"); } }
方案二:App Groups + UserDefaults(仅适合小数据,比如状态通知)
如果只是要传广播的状态(比如“广播已开始”“广播已结束”),用UserDefaults就够了,但绝对不要用它存音频数据——UserDefaults设计来存小数据的,存大数据会导致性能问题甚至崩溃:
- 在Extension里更新状态:
let defaults = UserDefaults(suiteName: Self.appGroupID)! defaults.set(true, forKey: "broadcast_is_running") defaults.synchronize() // 确保数据立刻同步 // 同样发Darwin通知告诉主App去读
- 主App里监听通知后读取UserDefaults的值就行,再传给Flutter。
给你的几个新手提示
- 一定要清理共享文件:每次读完音频文件就删掉,不然时间长了会占满设备空间
- 检查签名和App Groups配置:如果运行时提示无法访问共享容器,大概率是两个Target的App Groups标识不一致,或者签名的Team不对
- 注意CMSampleBuffer的内存管理:处理完音频样本后,确保相关的Buffer都正确释放,避免Extension内存过高被系统杀掉
- 测试的时候要先启动主App,再开启屏幕录制(因为Extension是在录制启动时才启动的)
备注:内容来源于stack exchange,提问作者falali
相关产品推荐
相关产品推荐

