iOS设备作为Icecast源客户端发送音频仅传8秒问题求助
这个问题我之前帮开发者排查过好几次——iOS向Icecast推流只传前几秒,核心原因确实是没有用HTTP分块传输(chunked transfer encoding)。Icecast服务器对于非分块的PUT请求,会读取到默认缓冲区大小(刚好对应你说的8秒音频)就认为传输结束,直接断开连接了。下面是具体的解决步骤和代码示例:
1. 配置正确的HTTP请求头
首先要给请求添加分块传输的头信息,同时指定正确的音频格式,还有Icecast的源认证信息(如果你的服务器设置了用户名密码):
let mountPointName = "myMountPoint" guard let url = URL(string: "http://localhost:8000/\(mountPointName)") else { print("无效的Icecast服务器地址") return } var request = URLRequest(url: url) request.httpMethod = "PUT" // 关键:开启分块传输 request.setValue("chunked", forHTTPHeaderField: "Transfer-Encoding") // 根据你的音频格式设置Content-Type,比如MP3用audio/mpeg,AAC用audio/aac request.setValue("audio/mpeg", forHTTPHeaderField: "Content-Type") // 如果Icecast配置了源认证,添加Authorization头 let sourceCredentials = "source:yourSourcePassword" // 替换成你的源用户名和密码 if let credentialsData = sourceCredentials.data(using: .utf8) { let base64Credentials = credentialsData.base64EncodedString() request.setValue("Basic \(base64Credentials)", forHTTPHeaderField: "Authorization") }
2. 使用流式上传任务(而非普通Data任务)
普通的dataTask(with:)会把整个音频数据一次性发送,不符合分块要求。我们需要用uploadTask(withStreamedRequest:)来创建流式上传任务,让系统自动分块发送数据:
首先,让你的类遵守URLSessionTaskDelegate协议,然后创建会话和任务:
// 初始化URLSession,指定代理 let session = URLSession(configuration: .default, delegate: self, delegateQueue: .main) // 创建流式上传任务 let uploadTask = session.uploadTask(withStreamedRequest: request) uploadTask.resume()
3. 实现代理方法提供音频数据流
接下来要实现URLSessionTaskDelegate的needNewBodyStream方法,给任务提供音频的输入流——如果是本地音频文件,可以直接用文件句柄的流;如果是实时录制的音频,需要自定义流来逐块传输数据。
示例1:传输本地音频文件
func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { // 替换成你的本地音频文件路径 guard let audioFileURL = Bundle.main.url(forResource: "yourAudioFile", withExtension: "mp3") else { completionHandler(nil) print("找不到音频文件") return } do { // 打开文件句柄获取输入流 let fileHandle = try FileHandle(forReadingFrom: audioFileURL) completionHandler(fileHandle.inputStream) } catch { print("打开文件失败:\(error.localizedDescription)") completionHandler(nil) } }
示例2:传输实时录制的音频
如果是麦克风实时录制的音频,你需要先把PCM数据编码成MP3/AAC,然后逐块写入到自定义流中。这里简化示例,假设你已经有编码好的音频数据块:
// 定义一个自定义输入流类,用于逐块提供音频数据 class ChunkedAudioInputStream: InputStream { private var dataChunks: [Data] = [] private var currentChunkIndex = 0 private var currentChunkOffset = 0 func addAudioChunk(_ chunk: Data) { dataChunks.append(chunk) // 有新数据时唤醒流读取 self.schedule(in: .main, forMode: .default) self.open() } override func read(_ buffer: UnsafeMutablePointer<UInt8>, maxLength len: Int) -> Int { guard currentChunkIndex < dataChunks.count else { // 所有块都读完,返回0表示结束 return 0 } let currentChunk = dataChunks[currentChunkIndex] let bytesAvailable = currentChunk.count - currentChunkOffset let bytesToRead = min(len, bytesAvailable) if bytesToRead > 0 { currentChunk.copyBytes(to: buffer, from: currentChunkOffset..<currentChunkOffset+bytesToRead) currentChunkOffset += bytesToRead // 当前块读完,切换到下一块 if currentChunkOffset == currentChunk.count { currentChunkIndex += 1 currentChunkOffset = 0 } return bytesToRead } return 0 } } // 在你的录制回调中添加数据块 let audioInputStream = ChunkedAudioInputStream(data: Data()) func audioRecorderDidOutputEncodedData(_ encodedData: Data) { audioInputStream.addAudioChunk(encodedData) } // 实现代理方法返回自定义流 func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void) { completionHandler(audioInputStream) }
4. 检查Icecast服务器配置
最后要确保你的Icecast配置文件(icecast.xml)允许源连接:
- 确认挂载点
<mount-name>和你代码中的mountPointName一致 - 确保
<allow-source>包含iOS设备的IP(本地测试可以设为127.0.0.1) - 检查
<source-password>和你代码中的认证密码匹配
你可以通过Icecast的管理页面(默认http://localhost:8000/admin)查看挂载点的连接状态,确认数据是否持续传输。
内容的提问来源于stack exchange,提问作者TheTiger

