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

Swift对接ElevenLabs WebSocket流式音频播放异常及报错排查

问题:ElevenLabs WebSocket流式音频播放中断及报错修复

问题现象

通过Swift与ElevenLabs建立WebSocket连接,传入文本接收流式音频分片,期望收到首个分片后立即连续播放,但实际仅播放少量内容就停止,同时出现两个报错:

WebSocket receive error: Error Domain=NSPOSIXErrorDomain Code=57 "Socket is not connected"
Audio player error: Error Domain=NSOSStatusErrorDomain Code=1954115647 "(null)"

相关代码

class WebSocketViewModel: NSObject, ObservableObject, URLSessionWebSocketDelegate {
    private var webSocketTask: URLSessionWebSocketTask?
    private var player: AVAudioPlayer?
    private var audioBuffer = Data()

    func connectAndPlay() {
        let urlString = "wss://api.elevenlabs.io/v1/text-to-speech/voice-id/stream-input?model_id=eleven_turbo_v2"
           guard let url = URL(string: urlString) else {
               print("Invalid URL")
               return
           }
           
           var request = URLRequest(url: url)

        let session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
        webSocketTask = session.webSocketTask(with: request)
        webSocketTask?.resume()
        sendInitialMessages(apiKey: apiKey)
    }

    private func sendInitialMessages(apiKey: String) {
        let bosMessage: [String: Any] = [
            "text": " ",
            "voice_settings": [
                "stability": 0.5,
                "similarity_boost": 0.8
            ],
            "xi_api_key": apiKey
        ]

        sendMessage(bosMessage)

        let textMessage: [String: Any] = [
            "text": "Hello World this is just a random message for testing purposes",
        ]

        sendMessage(textMessage)

        let eosMessage: [String: Any] = [
            "text": ""
        ]

        sendMessage(eosMessage)
    }

    private func sendMessage(_ message: [String: Any]) {
        do {
            let messageData = try JSONSerialization.data(withJSONObject: message, options: [])
            let messageString = String(data: messageData, encoding: .utf8) ?? ""
            let webSocketMessage = URLSessionWebSocketTask.Message.string(messageString)
            webSocketTask?.send(webSocketMessage) { error in
                if let error = error {
                    print("WebSocket send error: \(error)")
                }
            }
        } catch {
            print("JSON serialization error: \(error)")
        }
    }


    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol `protocol`: String?) {
        print("WebSocket connection opened")
        receiveMessages()
    }

    func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
        print("WebSocket connection closed")
    }

    private func receiveMessages() {
        webSocketTask?.receive { [weak self] result in
            switch result {
            case .failure(let error):
                print("WebSocket receive error: \(error)")
            case .success(let message):
                switch message {
                case .string(let text):
                    self?.handleMessage(text)
                default:
                    break
                }
                self?.receiveMessages()
            }
        }
    }

    private func handleMessage(_ text: String) {
        guard let data = text.data(using: .utf8),
              let response = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return }

        if let audioBase64 = response["audio"] as? String {
                    addToAudioBuffer(audioBase64)
                    if player == nil || !player!.isPlaying {
                        playAudio()
                    }
                }

        if let isFinal = response["isFinal"] as? Bool, isFinal {
            print("Audio generation is complete.")
        }

    }
    
    private func addToAudioBuffer(_ base64String: String) {
           guard let audioData = Data(base64Encoded: base64String) else {
               print("Failed to decode base64 audio data")
               return
           }
           audioBuffer.append(audioData)
       }

    private func playAudio() {
        guard !audioBuffer.isEmpty else {
                    print("Audio buffer is empty")
                    return
                }

                do {
                    player = try AVAudioPlayer(data: audioBuffer)
                    player?.play()
                } catch {
                    print("Audio player error: \(error)")
                }

                audioBuffer.removeAll()
    }
}

问题根源

  1. Socket连接错误(Code57):在WebSocket未完全建立连接时就发送消息,导致连接被强制中断。
  2. 音频播放失败(Code1954115647):AVAudioPlayer不支持流式追加音频数据,每次创建新实例并清空缓冲区的逻辑,会导致后续音频分片无法被播放,且旧的播放实例被覆盖后直接终止。
  3. 消息发送顺序混乱:连续异步发送WebSocket消息,可能导致消息顺序错误,触发ElevenLabs服务端的异常处理,提前关闭连接。

修复方案

1. 延迟消息发送时机

将消息发送逻辑移到WebSocket连接成功的回调中,确保连接完全建立后再发送数据:

func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol `protocol`: String?) {
    print("WebSocket connection opened")
    sendInitialMessages(apiKey: apiKey) // 移至此处
    receiveMessages()
}

func connectAndPlay() {
    let urlString = "wss://api.elevenlabs.io/v1/text-to-speech/voice-id/stream-input?model_id=eleven_turbo_v2"
    guard let url = URL(string: urlString) else {
        print("Invalid URL")
        return
    }
    
    var request = URLRequest(url: url)

    let session = URLSession(configuration: .default, delegate: self, delegateQueue: OperationQueue())
    webSocketTask = session.webSocketTask(with: request)
    webSocketTask?.resume()
    // 移除原来的sendInitialMessages调用
}

2. 改用AVAudioEngine实现流式播放

替换AVAudioPlayer为AVAudioEngine,支持实时追加音频分片:

private var audioEngine: AVAudioEngine?
private var audioPlayerNode: AVAudioPlayerNode?
private var audioFormat: AVAudioFormat?

// 初始化音频引擎
private func setupAudioEngine() {
    audioEngine = AVAudioEngine()
    audioPlayerNode = AVAudioPlayerNode()
    audioEngine?.attach(audioPlayerNode!)
    
    // ElevenLabs返回的音频为PCM 16bit 44.1kHz单声道,匹配格式
    let format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: 44100.0, channels: 1, interleaved: false)
    audioFormat = format
    
    audioEngine?.connect(audioPlayerNode!, to: audioEngine!.mainMixerNode, format: format)
    
    do {
        try audioEngine?.start()
    } catch {
        print("音频引擎启动失败: \(error)")
    }
}

// 修改消息处理逻辑,直接流式播放
private func handleMessage(_ text: String) {
    guard let data = text.data(using: .utf8),
          let response = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return }

    if let audioBase64 = response["audio"] as? String {
        guard let audioData = Data(base64Encoded: audioBase64) else {
            print("Base64音频解码失败")
            return
        }
        playStreamingAudio(audioData: audioData)
    }

    if let isFinal = response["isFinal"] as? Bool, isFinal {
        print("音频生成完成")
    }
}

// 流式播放音频分片
private func playStreamingAudio(audioData: Data) {
    guard let format = audioFormat, let playerNode = audioPlayerNode else {
        print("音频引擎未初始化")
        return
    }
    
    let frameCount = AVAudioFrameCount(audioData.count) / format.streamDescription.pointee.mBytesPerFrame
    guard let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount) else {
        print("创建音频缓冲区失败")
        return
    }
    buffer.frameLength = frameCount
    audioData.copyBytes(to: buffer.mutableAudioBufferList.pointee.mBuffers.mData!, count: Int(buffer.mutableAudioBufferList.pointee.mBuffers.mDataByteSize))
    
    if !playerNode.isPlaying {
        playerNode.play()
    }
    playerNode.scheduleBuffer(buffer)
}

记得在connectAndPlay中调用setupAudioEngine()。

3. 保证消息发送顺序

修改消息发送方法,添加完成回调,实现链式发送,避免异步发送导致的顺序混乱:

private func sendInitialMessages(apiKey: String) {
    let bosMessage: [String: Any] = [
        "text": " ",
        "voice_settings": [
            "stability": 0.5,
            "similarity_boost": 0.8
        ],
        "xi_api_key": apiKey
    ]
    
    sendMessage(bosMessage) { [weak self] in
        let textMessage: [String: Any] = [
            "text": "Hello World this is just a random message for testing purposes",
        ]
        self?.sendMessage(textMessage) { [weak self] in
            let eosMessage: [String: Any] = [
                "text": ""
            ]
            self?.sendMessage(eosMessage)
        }
    }
}

private func sendMessage(_ message: [String: Any], completion: (() -> Void)? = nil) {
    do {
        let messageData = try JSONSerialization.data(withJSONObject: message, options: [])
        let messageString = String(data: messageData, encoding: .utf8) ?? ""
        let webSocketMessage = URLSessionWebSocketTask.Message.string(messageString)
        webSocketTask?.send(webSocketMessage) { error in
            if let error = error {
                print("WebSocket发送错误: \(error)")
            }
            completion?()
        }
    } catch {
        print("JSON序列化错误: \(error)")
        completion?()
    }
}

4. 完善连接关闭后的资源清理

在WebSocket断开时清理音频资源和连接任务:

func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
    let reasonStr = String(data: reason ?? Data(), encoding: .utf8) ?? "无"
    print("WebSocket连接关闭,代码: \(closeCode),原因: \(reasonStr)")
    webSocketTask.cancel()
    self.webSocketTask = nil
    
    audioPlayerNode?.stop()
    audioEngine?.stop()
}

内容的提问来源于stack exchange,提问作者Sahil Singh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 23:49:54