You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.13 17:48:13