使用ReplayKit获取的CMSampleBuffer如何转换为m3u8格式并存储到指定路径?
实现CMSampleBuffer转本地M3U8存储的方案
要实现从ReplayKit采集的CMSampleBuffer生成M3U8,核心逻辑是先将音视频采样编码为H.264/AAC格式,再封装为TS切片,最后维护更新M3U8索引文件,具体步骤如下:
1. 导入依赖框架
需要用到系统的音视频编解码、文件处理相关框架:
import AVFoundation import VideoToolbox import AudioToolbox
2. 初始化全局配置参数
提前定义切片规则、存储路径、编码器等全局参数:
// 切片配置,单切片2秒适配低延迟实时播放 let kSegmentDuration: Double = 2 // 本地存储根路径,可替换为你指定的路径 let saveRootPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString let m3u8FilePath = saveRootPath.appendingPathComponent("screen_record_live.m3u8") // 运行时变量 var currentSegmentIndex = 0 var currentSegmentData: NSMutableData? var currentSegmentDuration: Double = 0 // 自定义H264编码器、AAC编码器实例 var h264Encoder: H264Encoder? var aacEncoder: AACEncoder?
3. 处理采集到的CMSampleBuffer
在系统回调的processSampleBuffer方法中完成编码、TS封装逻辑:
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer, with sampleBufferType: RPSampleBufferType) { switch sampleBufferType { case .video: // 视频帧送入H264编码器,得到NALU单元 h264Encoder?.encode(sampleBuffer: sampleBuffer, completion: { [weak self] naluData, isKeyFrame in guard let self = self else { return } // 遇到关键帧且当前切片达到预设时长时,完成上一个切片存储 if isKeyFrame && self.currentSegmentDuration >= kSegmentDuration { self.saveCurrentTSSegment() self.updateM3U8Index() // 重置当前切片状态 self.currentSegmentIndex += 1 self.currentSegmentData = NSMutableData() self.currentSegmentDuration = 0 } // 将NALU单元封装为TS视频包写入当前切片 if let videoTSPacket = TSPacketGenerator.generateVideoPacket(naluData: naluData, isKeyFrame: isKeyFrame) { self.currentSegmentData?.append(videoTSPacket) } // 累加切片时长 self.currentSegmentDuration += CMSampleBufferGetDuration(sampleBuffer).seconds }) case .audioApp, .audioMic: // 可先混合两路音频再送入AAC编码器,得到AAC帧 aacEncoder?.encode(sampleBuffer: sampleBuffer, completion: { [weak self] aacFrameData in guard let self = self else { return } // 将AAC帧封装为TS音频包写入当前切片 if let audioTSPacket = TSPacketGenerator.generateAudioPacket(aacData: aacFrameData) { self.currentSegmentData?.append(audioTSPacket) } }) @unknown default: break } }
4. 实现TS切片存储逻辑
将封装好的TS切片数据写入本地磁盘:
func saveCurrentTSSegment() { let segmentFilePath = saveRootPath.appendingPathComponent("seg_\(currentSegmentIndex).ts") try? currentSegmentData?.write(toFile: segmentFilePath, options: .atomic) }
5. 实现M3U8索引更新逻辑
按照标准HLS协议格式维护M3U8文件:
func updateM3U8Index() { var m3u8Content = "" // 首次生成时写入M3U8头部 if currentSegmentIndex == 0 { m3u8Content = """ #EXTM3U #EXT-X-VERSION:3 #EXT-X-TARGETDURATION:\(Int(kSegmentDuration + 1)) #EXT-X-MEDIA-SEQUENCE:0 """ } // 追加新切片的索引信息 m3u8Content += "#EXTINF:\(String(format: "%.3f", currentSegmentDuration)),\n" m3u8Content += "seg_\(currentSegmentIndex).ts\n" // 写入到本地路径 if let fileHandle = FileHandle(forWritingAtPath: m3u8FilePath) { fileHandle.seekToEndOfFile() fileHandle.write(m3u8Content.data(using: .utf8)!) } else { try? m3u8Content.write(toFile: m3u8FilePath, atomically: true, encoding: .utf8) } // 录制结束时需要追加结束标记,点播场景必须添加: // let endTag = "\n#EXT-X-ENDLIST" // try? endTag.append(toFile: m3u8FilePath, encoding: .utf8) }
注意事项
- 所有IO操作、编码操作不要放在主线程执行,避免阻塞ReplayKit的采样回调导致录制中断
- H.264编码生成的SPS、PPS数据要附加到每个关键帧的前面,否则单个TS切片解码时会出现花屏、无法播放的问题
- 封装TS数据包时要对齐音视频的PTS/DTS时间戳,避免最终播放时出现音画不同步的问题
- Broadcast Extension的内存上限为50MB,不要长时间缓存采样数据或切片数据,达到切片时长阈值后及时写入磁盘释放内存
- 本地部署HTTP服务器后,要确保M3U8和TS切片放在同一目录下,保证相对路径引用正确
内容的提问来源于stack exchange,提问作者Sam KC
相关产品推荐
相关产品推荐

